Merge branch 'stable-2.16'

* stable-2.16:
  dev-plugins.txt: Recommend ServletModule instead of HttpPluginModule
  AbandonUtil: Keep trying to abandon more changes despite single failure
  Fix polygerrit_plugin rule to include deps

Change-Id: Iddf4ea708fb057fa03b31e0ae81de16f90a9ef36
diff --git a/.gitignore b/.gitignore
index 319b3cf..c0e14ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,21 +14,36 @@
 /.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/replication
+!/plugins/reviewnotes
+!/plugins/singleusergroup
+!/plugins/webhooks
 /test_site
 /tools/format
-/.vscode
-/.ijwb
diff --git a/.gitmodules b/.gitmodules
index 8d75bcc..010b292 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -8,11 +8,21 @@
 	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
@@ -32,3 +42,8 @@
 	path = plugins/singleusergroup
 	url = ../plugins/singleusergroup
 	branch = .
+
+[submodule "plugins/webhooks"]
+	path = plugins/webhooks
+	url = ../plugins/webhooks
+	branch = .
diff --git a/BUILD b/BUILD
index cedc077..3989a75 100644
--- a/BUILD
+++ b/BUILD
@@ -1,10 +1,8 @@
-load(
-    "@bazel_tools//tools/jdk:default_java_toolchain.bzl",
-    "default_java_toolchain",
-)
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("//tools/bzl:pkg_war.bzl", "pkg_war")
 
+package(default_visibility = ["//visibility:public"])
+
 config_setting(
     name = "java9",
     values = {
@@ -13,44 +11,12 @@
 )
 
 config_setting(
-    name = "java10",
+    name = "java_next",
     values = {
-        "java_toolchain": ":toolchain_vanilla",
+        "java_toolchain": "@bazel_tools//tools/jdk:toolchain_vanilla",
     },
 )
 
-# TODO(davido): Switch to consuming it from @bazel_tool//tools/jdk:absolute_javabase
-# when new Bazel version is released with this change included:
-# https://github.com/bazelbuild/bazel/issues/6012
-# https://github.com/bazelbuild/bazel/commit/0173bdbf7bdd1874379d4dd3eb70d5321e0f1816
-# As the interim use a hack that works around it by putting the variable reference
-# behind a select
-config_setting(
-    name = "use_absolute_javabase",
-    values = {"define": "USE_ABSOLUTE_JAVABASE=true"},
-)
-
-java_runtime(
-    name = "absolute_javabase",
-    java_home = select({
-        "//conditions:default": "",
-        ":use_absolute_javabase": "$(ABSOLUTE_JAVABASE)",
-    }),
-    visibility = ["//visibility:public"],
-)
-
-# TODO(davido): Switch to consuming it from @bazel_tool//tools/jdk:toolchain_vanilla
-# when my change is included in released Bazel version:
-# https://github.com/bazelbuild/bazel/commit/0bef68e054eccecd690e5d9f46db8a0c4b2d887a
-default_java_toolchain(
-    name = "toolchain_vanilla",
-    forcibly_disable_header_compilation = True,
-    javabuilder = ["@bazel_tools//tools/jdk:VanillaJavaBuilder_deploy.jar"],
-    jvm_opts = [],
-)
-
-package(default_visibility = ["//visibility:public"])
-
 genrule(
     name = "gen_version",
     outs = ["version.txt"],
@@ -77,15 +43,9 @@
 )
 
 pkg_war(
-    name = "polygerrit",
-    ui = "polygerrit",
-)
-
-pkg_war(
     name = "release",
     context = ["//plugins:core"],
     doc = True,
-    ui = "ui_optdbg_r",
 )
 
 pkg_war(
@@ -103,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..368e929 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,7 +52,6 @@
 license_map(
     name = "js_licenses",
     targets = [
-        "//gerrit-gwtui:ui_module",
         "//polygerrit-ui/app:polygerrit_ui",
     ],
 )
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 370a891..2e7cf17 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
@@ -1198,8 +1204,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/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 6fe24815..b15aea7 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -27,7 +27,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.
@@ -144,7 +144,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/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 7a458f9..8479a8e 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"`::
 +
@@ -1175,8 +1172,7 @@
 
 [[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.
 
@@ -1218,6 +1214,15 @@
 +
 Default is true.
 
+[[change.api.excludeMergeableInChangeInfo]]change.api.excludeMergeableInChangeInfo::
++
+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.
+
 [[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
 +
 Show assignee field in changes table. If set to false, assignees will
@@ -1433,13 +1438,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 +1687,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
 
@@ -2222,11 +2015,16 @@
 +
 Defaults to "Report Bug".
 
-[[gerrit.disableReverseDnsLookup]]gerrit.disableReverseDnsLookup::
+[[gerrit.enableReverseDnsLookup]]gerrit.enableReverseDnsLookup::
 +
-Disables reverse DNS lookup during computing ref log entry for identified user.
+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.
 +
-Defaults to false.
+Enabling reverse DNS lookup can cause performance issues on git push when
+the reverse DNS lookup is slow.
++
+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::
 +
@@ -2866,8 +2664,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
@@ -3562,9 +3359,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::
 +
@@ -3917,9 +3714,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
@@ -4710,103 +4507,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
 
@@ -5112,6 +4812,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`
 
@@ -5151,11 +4883,6 @@
 
 The format is one Base-64 encoded public key per line.
 
-
-== Configuring the Polygerrit UI
-
-Please see link:dev-polygerrit.html[UI] on configuring the Polygerrit UI.
-
 === Configurable Parameters
 
 site_path::
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 190d003..3a80910 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].
@@ -61,6 +65,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
 
@@ -74,6 +90,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
 
@@ -189,8 +213,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] |
@@ -211,16 +234,6 @@
 link:https://gerrit.googlesource.com/plugins/changemessage/+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]
-
 [[egit]]
 === egit
 
@@ -247,6 +260,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
 
@@ -263,14 +287,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
 
@@ -489,13 +505,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
@@ -633,6 +657,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 4456484..8962632 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 #submit-type[submit type].  Valid
 values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
 'merge always' and 'cherry pick'.  The default is 'merge if necessary'.
 
@@ -410,3 +479,95 @@
 
 SEARCHBOX
 ---------
+
+[[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 no merge commits are produced. All merges must
+be handled 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]]
+If `Allow content merges` is enabled, Gerrit will try
+to do a content merge when a path conflict occurs.
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 0ecc820..bec1984 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -6,7 +6,7 @@
 To build Gerrit from source, you need:
 
 * A Linux or macOS system (Windows is not supported at this time)
-* A JDK for Java 8|9|10
+* 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]
@@ -14,27 +14,55 @@
 * zip, unzip
 * gcc
 
-[[Java 10 support]]
-Java 10 is supported through vanilla java toolchain
+[[Java 10 and newer version support]]
+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, specify vanilla java toolchain and provide
-path to Java 10 home:
+To build Gerrit with Java 10 and newer, specify vanilla java toolchain and
+provide the path to JDK home:
 
 ```
-  $ bazel build --host_javabase=:absolute_javabase \
+  $ bazel build \
     --define=ABSOLUTE_JAVABASE=<path-to-java-10> \
-    --define=USE_ABSOLUTE_JAVABASE=true \
-    --host_java_toolchain=//:toolchain_vanilla \
-    --java_toolchain=//:toolchain_vanilla \
+    --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
+    --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
+    --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
     :release
 ```
 
-Note that the following options must be added to `container.javaOptions`
-in `$gerrit_site/etc/gerrit.config` to run Gerrit with Java 10:
+To run the tests, `--javabase` option must be passed as well, because
+bazel test runs the test using the target javabase:
+
+```
+  $ bazel test \
+    --define=ABSOLUTE_JAVABASE=<path-to-java-10> \
+    --javabase=@bazel_tools//tools/jdk:absolute_javabase \
+    --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
+    --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
+    --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
+    //...
+```
+
+To avoid passing all those options on every Bazel build invocation,
+they could be added to ~/.bazelrc resource file:
+
+```
+$ cat << EOF > ~/.bazelrc
+> build --define=ABSOLUTE_JAVABASE=<path-to-java-10>
+> build --javabase=@bazel_tools//tools/jdk:absolute_javabase
+> build --host_javabase=@bazel_tools//tools/jdk:absolute_javabase
+> build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla
+> build --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla
+> EOF
+```
+
+Now, invoking Bazel with just `bazel build :release` would include
+all those options.
+
+Note that the follow option must be added to `container.javaOptions`
+in `$gerrit_site/etc/gerrit.config` to run Gerrit with Java 10|11|...:
 
 ```
 [container]
-  javaOptions = --add-modules java.activation
   javaOptions = --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
 ```
 
@@ -51,13 +79,12 @@
       :release
 ```
 
-Note that the following option must be added to `container.javaOptions`
+Note that the follow option must be added to `container.javaOptions`
 in `$gerrit_site/etc/gerrit.config` to run Gerrit with Java 9:
 
 ```
 [container]
-  javaOptions = --add-modules java.activation \
-      --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
+  javaOptions = --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
 ```
 
 [[build]]
@@ -65,7 +92,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
@@ -84,8 +111,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
@@ -99,7 +126,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:
 
 ----
@@ -114,7 +141,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
@@ -127,7 +154,8 @@
   bazel-genfiles/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
@@ -258,31 +286,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 e3ab7ce..e40af24 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -250,7 +250,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
@@ -269,11 +268,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).
 
 
@@ -322,31 +321,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
 
@@ -379,20 +391,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 c335a2d..67ced54 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 require
 
 [[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 `cookbook-plugin` should be added in the local
-copy of the `gerrit_gwt_debug` configuration:
-
-----
-  com.googlesource.gerrit.plugins.cookbook.HelloForm \
-  -src ${resource_loc:/gerrit}/plugins/cookbook-plugin/src/main/java \
-  -- --console-log [...]
-----
-
-After doing that, both the Gerrit core and plugin GWT modules can be activated
-during SDM (debug session)[http://imgur.com/HFXZ5No].
-
 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..b87cbf4 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -24,6 +24,13 @@
 . Install it.
 . Restart IntelliJ.
 
+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 IntelliJ project
 
 . Go to *File -> Import Bazel Project*.
@@ -100,7 +107,9 @@
 === Copyright
 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. Then go to *File -> Settings -> Editor -> Copyright -> Copyright Profiles*,
+and import `Gerrit_Copyright.xml` to IntelliJ in case it doesn't pick the
+copyright up automatically.
 
 === File header
 By default, IntelliJ adds a file header containing the name of the author and
@@ -177,10 +186,6 @@
 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
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 0198790..1e54cbf 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -325,10 +325,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]
 ----
@@ -718,7 +718,6 @@
 
 [source,java]
 ----
-@Singleton
 public class SampleOperator
     implements ChangeQueryBuilder.ChangeOperatorFactory {
   public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
@@ -751,7 +750,6 @@
 new `has:sample_pluginName` operand is shown below:
 
 ====
-  @Singleton
   public class SampleHasOperand implements ChangeHasOperandFactory {
     public static class Module extends AbstractModule {
       @Override
@@ -801,56 +799,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;
@@ -863,9 +917,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);
@@ -893,6 +947,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]]
@@ -1302,7 +1373,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:
@@ -1515,9 +1586,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) {
@@ -1878,317 +1948,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
 
@@ -2923,6 +2682,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-polygerrit.txt b/Documentation/dev-polygerrit.txt
deleted file mode 100644
index 5621d32..0000000
--- a/Documentation/dev-polygerrit.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-= PolyGerrit - GUI
-
-== Configuring
-
-By default both GWT and PolyGerrit UI are available to users.
-
-To make PolyGerrit the default UI but keep GWT as a secondary UI:
-----
-[gerrit]
-        ui = POLYGERRIT
-----
-
-To disable GWT but not PolyGerrit:
-----
-[gerrit]
-        enableGwtUi = false
-        enablePolyGerrit = true
-----
-
-To enable GWT but not PolyGerrit:
-----
-[gerrit]
-        enableGwtUi = true
-        enablePolyGerrit = false
-----
-
-To switch to the PolyGerrit UI you have to add `?polygerrit=1` in the URL.
-
-for example https://gerrit.example.org/?polygerrit=1
-
-To disable PolyGerrit UI, change 1 to 0, which will take you back to GWT UI.
-
-
-More information can be found in the link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/[README]
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/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 dbca368..0885da1 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -9,10 +9,6 @@
 +
 Gerrit is not yet compatible with Java 9 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..17c9a61 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
@@ -548,7 +548,8 @@
 ----
 Alternatively, click *Ready* 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]]
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/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/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 ad613a5..cb80c74 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -141,3 +141,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-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 f76b5e4..23030a4 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -48,198 +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 no merge commits are produced. All merges must
-be handled 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]]
-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..e6cd822 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -1044,10 +1044,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 +1055,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/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index de5f278..c326b66 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -2141,12 +2141,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.txt#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\}
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index d500480..a994711 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.
@@ -5760,12 +5773,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].
@@ -5773,8 +5788,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[
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 3ec989e..3ea3ba1 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": {
@@ -1805,9 +1802,6 @@
 |`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]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index a36947b..f69c4ae 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/WORKSPACE b/WORKSPACE
index a604c92..b4b1a64 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,6 @@
 load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "maven_jar")
-load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION")
 load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
 
 http_archive(
@@ -108,24 +107,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(
@@ -146,61 +145,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()
@@ -233,13 +177,6 @@
 )
 
 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",
@@ -247,9 +184,9 @@
 
 maven_jar(
     name = "gwtorm-client",
-    artifact = "com.google.gerrit:gwtorm:1.18",
-    sha1 = "f326dec463439a92ccb32f05b38345e21d0b5ecf",
-    src_sha1 = "e0b973d5cafef3d145fa80cdf032fcead1186d29",
+    artifact = "com.google.gerrit:gwtorm:1.20",
+    sha1 = "a4809769b710bc8ce3f203125630b8419f0e58b0",
+    src_sha1 = "cb63296276ce3228b2d83a37017a99e38ad8ed42",
 )
 
 maven_jar(
@@ -267,6 +204,12 @@
 )
 
 maven_jar(
+    name = "guava-failureaccess",
+    artifact = "com.google.guava:failureaccess:1.0.1",
+    sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+)
+
+maven_jar(
     name = "j2objc",
     artifact = "com.google.j2objc:j2objc-annotations:1.1",
     sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
@@ -355,8 +298,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(
@@ -391,6 +340,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(
@@ -543,7 +519,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",
@@ -586,50 +562,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.3"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "e7eae562942315a983eea3e191b72d755c153620",
+    sha1 = "8edb6675b9c09ffdcc19937428e7ef1e3d066e12",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "ed193d86e0af90cc2342aedbe73c5d86b03fa09b",
+    sha1 = "b88c1bb7f149f6d2cc03898359283e57b08f39cc",
 )
 
 # Transitive dependency of commons-compress
@@ -726,14 +702,30 @@
     sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
 )
 
+GITILES_VERS = "0.2-7"
+
 maven_jar(
     name = "blame-cache",
-    artifact = "com/google/gitiles:blame-cache:0.2-7",
+    artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
     attach_source = False,
     repository = GERRIT,
     sha1 = "8170f33b8b1db6f55e41d7069fa050a4d102a62b",
 )
 
+maven_jar(
+    name = "gitiles-servlet",
+    artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
+    repository = GERRIT,
+    sha1 = "f23b22cb27fe5c4a78f761492082159d17873f57",
+)
+
+# 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",
@@ -867,30 +859,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.42"
+TRUTH_VERS = "0.43"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "b5768f644b114e6cf5c3962c2ebcb072f788dcbb",
+    sha1 = "0cb9105957368f68d5fd771045bfa27d4a534836",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "4d01dfa5b3780632a3d109e14e101f01d10cce2c",
+    sha1 = "0707dd0b4eb2101aa85c4bb7caf52d9ae32f0a43",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "c231e6735aa6c133c7e411ae1c1c90b124900a8b",
+    sha1 = "67d833098345fc744c6fb2c38c739557b2d73742",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "c41d22e8b4a61b4171e57c44a2959ebee0091a14",
+    sha1 = "b9641436e7b48b3642ac7aa7b8f3d1b9d04bc44b",
 )
 
 maven_jar(
@@ -1045,31 +1037,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",
@@ -1135,6 +1102,38 @@
     sha1 = "65bd0cacc9c79a21c6ed8e9f588577cd3c2f85b9",
 )
 
+maven_jar(
+    name = "javax-activation",
+    artifact = "javax.activation:activation:1.1.1",
+    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.
@@ -1306,8 +1305,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/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md
new file mode 100644
index 0000000..ad23140
--- /dev/null
+++ b/contrib/mitm-ui/README.md
@@ -0,0 +1,58 @@
+# 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. 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..77f209e
--- /dev/null
+++ b/contrib/mitm-ui/mitm-docker.sh
@@ -0,0 +1,42 @@
+#!/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 ${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..992ef07
--- /dev/null
+++ b/contrib/mitm-ui/mitm-plugins.sh
@@ -0,0 +1,33 @@
+#!/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 &
+
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --plugins ${absolute_plugin_paths} --strip_assets"
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..4fa8958
--- /dev/null
+++ b/contrib/mitm-ui/mitm-serve-app-dev.sh
@@ -0,0 +1,13 @@
+#!/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 )"
+
+${mitm_dir}/dev-chrome.sh &
+
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/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..4acae7f
--- /dev/null
+++ b/contrib/mitm-ui/mitm-single-plugin.sh
@@ -0,0 +1,31 @@
+#!/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 &
+
+${mitm_dir}/mitm-docker.sh -v ${plugin_root}:${plugin_root} "serve-app-dev.py --plugins ${plugin} --strip_assets --plugin_root ${plugin_root}"
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..18e9de1
--- /dev/null
+++ b/contrib/mitm-ui/serve-app-dev.py
@@ -0,0 +1,156 @@
+# 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 re
+import argparse
+import os.path
+import json
+import mimetypes
+
+class Server:
+    def __init__(self, devpath, plugins, pluginroot, assets, strip_assets, theme):
+        if devpath:
+            print("Serving app from " + devpath)
+        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.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 = ""
+    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]
+        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)
+        flow.response.content = server.readfile(localfile)
+        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("--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", type=str, help="Path to the default site theme to be used.")
+args = parser.parse_args()
+server = Server(expandpath(args.app) + '/',
+                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/gerrit-gwtdebug/BUILD b/gerrit-gwtdebug/BUILD
deleted file mode 100644
index f564745..0000000
--- a/gerrit-gwtdebug/BUILD
+++ /dev/null
@@ -1,16 +0,0 @@
-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 c644b85..0000000
--- a/gerrit-gwtui-common/BUILD
+++ /dev/null
@@ -1,62 +0,0 @@
-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 9deed9b..0000000
--- a/gerrit-gwtui/BUILD
+++ /dev/null
@@ -1,41 +0,0 @@
-load(
-    "//tools/bzl:gwt.bzl",
-    "gen_ui_module",
-    "gwt_genrule",
-    "gwt_user_agent_permutations",
-)
-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()
-
-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 63b1ce2..0000000
--- a/gerrit-plugin-gwtui/BUILD
+++ /dev/null
@@ -1,83 +0,0 @@
-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-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..5286ceb 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,9 @@
 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 +141,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 +164,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 +181,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 +195,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 +214,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 +244,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 +252,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 +268,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 +279,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 +334,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.
@@ -402,18 +401,16 @@
     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();
@@ -424,6 +421,7 @@
 
     adminRestSession = new RestSession(server, admin);
     userRestSession = new RestSession(server, user);
+    anonymousRestSession = new RestSession(server, null);
 
     initSsh();
 
@@ -437,7 +435,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 +524,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 +553,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.getId(), p);
     Repository repo = repoManager.openRepository(p);
     toClose.add(repo);
     return inProcessProtocol.register(ctx, repo).toString();
@@ -582,13 +564,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 +605,7 @@
   }
 
   protected PushOneCommit.Result createChange(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     return result;
@@ -641,7 +621,6 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "parent 1",
@@ -654,7 +633,6 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "parent 2",
@@ -663,11 +641,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "merge",
-            ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
+            admin.getIdent(), 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 +656,7 @@
       String content)
       throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(db, admin.getIdent(), repo, commitMsg, fileName, content).to(ref);
+        pushFactory.create(admin.getIdent(), repo, commitMsg, fileName, content).to(ref);
     result.assertOkStatus();
     return result;
   }
@@ -701,8 +675,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.getIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master");
   }
 
@@ -714,7 +687,7 @@
       String content,
       String topic)
       throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), repo, subject, fileName, content);
     return push.to("refs/for/" + branch + "%topic=" + name(topic));
   }
 
@@ -733,7 +706,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 +741,7 @@
       String content)
       throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, testAccount.getIdent(), repo, subject, fileName, content, changeId);
+        pushFactory.create(testAccount.getIdent(), repo, subject, fileName, content, changeId);
     return push.to(ref);
   }
 
@@ -794,28 +767,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.getId());
+    return atrScope.get();
   }
 
   protected Account getAccount(Account.Id accountId) {
@@ -828,14 +781,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 +808,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 +891,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 +932,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 +943,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 +1005,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 +1029,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 +1047,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 +1061,7 @@
   }
 
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     return push.to(ref);
   }
 
@@ -1140,7 +1088,7 @@
   }
 
   protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
-    return changeDataFactory.create(db, project, psId.getParentKey()).patchSet(psId);
+    return changeDataFactory.create(project, psId.getParentKey()).patchSet(psId);
   }
 
   protected IdentifiedUser user(TestAccount testAccount) {
@@ -1169,46 +1117,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 +1139,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);
@@ -1646,7 +1537,7 @@
 
     private ProjectConfigUpdate(Project.NameKey projectName) throws Exception {
       metaDataUpdate = metaDataUpdateFactory.create(projectName);
-      projectConfig = ProjectConfig.read(metaDataUpdate);
+      projectConfig = projectConfigFactory.read(metaDataUpdate);
     }
 
     public ProjectConfig getConfig() {
@@ -1693,4 +1584,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..1a6ed37 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,6 +42,7 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -54,9 +56,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.getId());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -82,7 +86,7 @@
     if (record) {
       accountsModifyingEmailStrategy.add(account);
     }
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
     prefs.emailStrategy = strategy;
     gApi.accounts().self().setPreferences(prefs);
@@ -99,9 +103,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 +132,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
@@ -354,7 +365,7 @@
         assignee = testAccount("assignee");
 
         watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
-        setApiUser(watchingProjectOwner);
+        requestScopeOperations.setApiUser(watchingProjectOwner.getId());
         watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
 
         for (NotifyType watch : NotifyType.values()) {
@@ -362,7 +373,7 @@
             continue;
           }
           TestAccount watcher = testAccount(watch.toString());
-          setApiUser(watcher);
+          requestScopeOperations.setApiUser(watcher.getId());
           watch(
               allProjects.get(),
               pwi -> {
@@ -455,9 +466,9 @@
       if (pushOptions != null) {
         ref = ref + '%' + Joiner.on(',').join(pushOptions);
       }
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       repo = cloneProject(project, owner);
-      PushOneCommit push = pushFactory.create(db, owner.getIdent(), repo);
+      PushOneCommit push = pushFactory.create(owner.getIdent(), repo);
       result = push.to(ref);
       result.assertOkStatus();
       changeId = result.getChangeId();
@@ -477,33 +488,38 @@
     StagedChange(String ref) throws Exception {
       super(ref);
 
-      setApiUser(starrer);
+      requestScopeOperations.setApiUser(starrer.getId());
       gApi.accounts().self().starChange(result.getChangeId());
 
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       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.getId());
     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.getId());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -511,7 +527,7 @@
 
   protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -519,7 +535,7 @@
 
   protected StagedChange stageAbandonedWipChange() throws Exception {
     StagedChange sc = stageWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     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..d85613c 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.common.errors.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,6 +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.gerrit.server.notedb.Sequences;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 57f166a..d61cf7a 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -18,6 +18,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",
@@ -27,6 +28,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",
@@ -35,7 +37,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:h2",
         "//lib:jimfs",
@@ -50,6 +51,7 @@
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/mina:sshd",
+        "//lib/mockito",
         "//prolog:gerrit-prolog-common",
     ],
 )
@@ -101,21 +103,26 @@
         "//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/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 0d473af..d39edec 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -20,7 +20,6 @@
 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;
@@ -59,7 +58,7 @@
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) throws IOException {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
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 050de49..3a49f46 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() {
@@ -307,11 +321,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.
@@ -319,15 +333,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;
     }
   }
@@ -344,8 +361,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.
@@ -357,7 +372,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");
@@ -383,8 +397,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);
   }
@@ -394,8 +407,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));
@@ -408,12 +420,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() {
@@ -508,8 +521,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 {
@@ -522,12 +533,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);
   }
@@ -552,7 +577,6 @@
   private ExecutorService daemonService;
   private Injector testInjector;
   private String url;
-  private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
   private GerritServer(
@@ -570,12 +594,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());
   }
 
@@ -583,10 +601,6 @@
     return url;
   }
 
-  InetSocketAddress getSshdAddress() {
-    return sshdAddress;
-  }
-
   InetSocketAddress getHttpAddress() {
     return httpAddress;
   }
@@ -612,21 +626,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) {
@@ -635,40 +637,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..fdd1fce 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,7 +32,8 @@
   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(
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 6ee5c09..844d6c8 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -21,52 +21,35 @@
 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 +68,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) {
+        schemaCreator.ensureCreated();
+      } catch (OrmException | IOException | ConfigInvalidException e) {
         throw new OrmRuntimeException(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 de8d10c..7a79ce4 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -21,14 +21,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 +37,13 @@
 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.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 +53,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 +119,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 +145,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 +155,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 +169,7 @@
     }
 
     private Context newContinuingContext() {
-      return new Context(schemaFactory, userFactory, accountId, project);
+      return new Context(userFactory, accountId, project);
     }
 
     @Override
@@ -190,11 +177,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 +190,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 +200,7 @@
     @Inject
     Upload(
         TransferConfig transferConfig,
-        DynamicSet<UploadPackInitializer> uploadPackInitializers,
+        PluginSetContext<UploadPackInitializer> uploadPackInitializers,
         DynamicSet<PreUploadHook> preUploadHooks,
         UploadValidators.Factory uploadValidatorsFactory,
         ThreadLocalRequestContext threadContext,
@@ -267,9 +249,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,7 +259,7 @@
     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;
@@ -290,7 +270,7 @@
         ProjectCache projectCache,
         AsyncReceiveCommits.Factory factory,
         TransferConfig config,
-        DynamicSet<ReceivePackInitializer> receivePackInitializers,
+        PluginSetContext<ReceivePackInitializer> receivePackInitializers,
         DynamicSet<PostReceiveHook> postReceiveHooks,
         ThreadLocalRequestContext threadContext,
         PermissionBackend permissionBackend) {
@@ -339,9 +319,8 @@
         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));
 
         rp.setPostReceiveHook(PostReceiveHookChain.newChain(Lists.newArrayList(postReceiveHooks)));
         return rp;
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 5e45df2..f16f1d3 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,10 +27,8 @@
 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;
@@ -43,7 +40,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 +73,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 +86,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 +134,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 +150,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 +161,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 +169,6 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
-        notesMigration,
-        db,
         i,
         testRepo,
         SUBJECT,
@@ -210,26 +182,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 +196,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 +209,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 +220,6 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
-        notesMigration,
-        db,
         i,
         testRepo,
         subject,
@@ -288,20 +231,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;
@@ -436,22 +375,15 @@
       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 {
       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..d4fd318 100644
--- a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -19,7 +19,7 @@
 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;
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) throws IOException {
     // do nothing
   }
 
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..db93009 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;
@@ -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/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..113c19c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -17,13 +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.gerrit.server.notedb.Sequences;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -50,8 +50,8 @@
   }
 
   @Override
-  public MoreAccountOperations account(Account.Id accountId) {
-    return new MoreAccountOperationsImpl(accountId);
+  public PerAccountOperations account(Account.Id accountId) {
+    return new PerAccountOperationsImpl(accountId);
   }
 
   @Override
@@ -100,28 +100,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()
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/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..ac818c9 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.common.errors.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,6 +26,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.gerrit.server.notedb.Sequences;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -60,8 +60,8 @@
   }
 
   @Override
-  public MoreGroupOperations group(AccountGroup.UUID groupUuid) {
-    return new MoreGroupOperationsImpl(groupUuid);
+  public PerGroupOperations group(AccountGroup.UUID groupUuid) {
+    return new PerGroupOperationsImpl(groupUuid);
   }
 
   @Override
@@ -101,25 +101,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())
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 aa99a4f..29f3f72 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -1,33 +1,13 @@
-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(
@@ -44,7 +24,6 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/org/eclipse/jgit:server",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
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..1be6353
--- /dev/null
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -0,0 +1,42 @@
+// 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,
+    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..b3da199 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;
@@ -36,12 +37,8 @@
     super(refPattern);
   }
 
-  // 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);
+  public ImmutableList<Permission> getPermissions() {
+    return permissions == null ? ImmutableList.of() : ImmutableList.copyOf(permissions);
   }
 
   public void setPermissions(List<Permission> list) {
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/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/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/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 5b18368..9669c375 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -15,15 +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 static org.apache.commons.codec.binary.Base64.decodeBase64;
 
 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;
@@ -38,8 +39,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;
@@ -48,9 +53,8 @@
 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;
@@ -60,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;
@@ -85,14 +88,22 @@
   protected static final String SETTINGS = "settings";
 
   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.toString())))
-        .toList();
+    return Streams.stream(field)
+        .map(JsonElement::toString)
+        .map(Base64::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 {
@@ -377,7 +388,6 @@
 
     private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
       try {
-        List<T> results = Collections.emptyList();
         String uri = getURI(index, SEARCH);
         Response response =
             performRequest(HttpPost.METHOD_NAME, uri, search, Collections.emptyMap());
@@ -388,34 +398,19 @@
               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);
       }
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index 8d23051..d5c586b 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -9,6 +9,7 @@
         "//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",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 1224c61..344e10c 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;
@@ -43,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;
@@ -61,7 +59,6 @@
 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;
@@ -93,20 +90,17 @@
   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());
@@ -118,7 +112,7 @@
     String insertIndex;
 
     try {
-      if (cd.change().getStatus().isOpen()) {
+      if (cd.change().isNew()) {
         insertIndex = OPEN_CHANGES;
         deleteIndex = CLOSED_CHANGES;
       } else {
@@ -186,7 +180,7 @@
   }
 
   @Override
-  protected String getDeleteActions(Id c) {
+  protected String getDeleteActions(Change.Id c) {
     if (client.adapter().usePostV5Type()) {
       return delete(ElasticQueryAdapter.POST_V5_TYPE, c);
     }
@@ -219,22 +213,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(Base64.decodeBase64(c.getAsString())));
+            parseProtoFrom(Base64.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/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/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index ea08f99..797c656 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -1,17 +1,7 @@
 load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL")
 load("//lib:guava.bzl", "GUAVA_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",
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/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 4f16e19..47ccb49 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;
@@ -312,9 +367,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.
    */
@@ -356,16 +410,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();
     }
@@ -376,31 +420,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();
     }
@@ -421,27 +450,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();
     }
@@ -472,11 +485,6 @@
     }
 
     @Override
-    public AddReviewerResult addReviewer(String in) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -492,22 +500,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();
     }
 
@@ -517,11 +512,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/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/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/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/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..4746273 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;
@@ -25,6 +22,5 @@
   public Boolean editGpgKeys;
   public String reportBugUrl;
   public String reportBugText;
-  public Set<UiType> webUis;
   public String primaryWeblinkName;
 }
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 8c2e566..3f64ddb 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -135,7 +135,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;
@@ -167,10 +167,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
@@ -199,8 +197,6 @@
             throw new UnsupportedOperationException();
           }
         };
-      }
-    };
   }
 
   /**
@@ -264,12 +260,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/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..f0c01de
--- /dev/null
+++ b/java/com/google/gerrit/git/BUILD
@@ -0,0 +1,9 @@
+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..520d0f2
--- /dev/null
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -0,0 +1,182 @@
+// 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);
+    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..198ddff
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/CommitSubject.java
@@ -0,0 +1,100 @@
+// 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 com.google.common.truth.Truth;
+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();
+    Truth.assertThat(commit.getFullMessage())
+        .named("commit message")
+        .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());
+    Truth.assertThat(timestampDiffMs).named("commit timestamp diff").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();
+    Truth.assertThat(commit).named("SHA1").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..0fd3b73
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/ObjectIdSubject.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.git.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ObjectIdSubject extends Subject<ObjectIdSubject, ObjectId> {
+  public static ObjectIdSubject assertThat(ObjectId objectId) {
+    return assertAbout(ObjectIdSubject::new).that(objectId);
+  }
+
+  private ObjectIdSubject(FailureMetadata metadata, ObjectId actual) {
+    super(metadata, actual);
+  }
+
+  public void hasName(String expectedName) {
+    isNotNull();
+    ObjectId objectId = actual();
+    Truth.assertThat(objectId.getName()).named("name").isEqualTo(expectedName);
+  }
+}
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/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/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 2294d0e..def4b30 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -11,6 +11,8 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//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",
@@ -28,7 +30,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
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..164f957
--- /dev/null
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.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.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.gwtorm.server.OrmException;
+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 (OrmException | PermissionBackendException 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..1ff8580 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;
@@ -103,19 +104,18 @@
         return;
       }
 
-      Account target;
+      Account.Id target;
       try {
-        target = accountResolver.find(runas);
+        target = accountResolver.resolve(runas).asUnique().getAccount().getId();
+      } catch (UnprocessableEntityException e) {
+        replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
+        return;
       } catch (OrmException | IOException | ConfigInvalidException 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..d6ff2ec 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;
@@ -36,7 +35,6 @@
 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 +58,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 +67,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 +87,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 +105,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 +141,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 +149,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);
@@ -219,7 +207,7 @@
   }
 
   private Optional<AuthResult> byPreferredEmail(String email) {
-    try (ReviewDb db = schema.open()) {
+    try {
       Optional<AccountState> match =
           queryProvider.get().byPreferredEmail(email).stream().findFirst();
       return auth(match);
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/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/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 aadfeba..aa5362b 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;
@@ -38,7 +37,6 @@
 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;
@@ -80,20 +78,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;
@@ -112,7 +105,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;
@@ -132,7 +124,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. */
@@ -268,43 +259,9 @@
 
       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());
     modules.add(new DropWizardMetricMaker.ApiModule());
     return Guice.createInjector(PRODUCTION, modules);
   }
@@ -312,7 +269,7 @@
   private Injector createCfgInjector() {
     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);
   }
@@ -370,7 +327,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);
           }
         });
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 430f0b5..a13078d 100644
--- a/java/com/google/gerrit/httpd/raw/BazelBuild.java
+++ b/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -147,11 +147,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..1e60d31 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;
@@ -32,7 +31,6 @@
 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 +51,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 +59,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 +123,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 +135,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;
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/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 fc6762b..e513620 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;
@@ -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 (!p.equals("/")) {
-          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 588f349..d6dca48 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 6193e45..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);
-    }
-    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 44c8966..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);
-    }
-  }
-
-  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 8d4e6dd..9d61c02 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -21,11 +21,11 @@
         "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
         "//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",
diff --git a/java/com/google/gerrit/index/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index beb9c07..f565e52 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -18,6 +18,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -60,6 +61,7 @@
 
   @FunctionalInterface
   public interface Getter<I, T> {
+    @Nullable
     T get(I input) throws OrmException, IOException;
   }
 
@@ -133,6 +135,7 @@
    * @return the field value(s) to index.
    * @throws OrmException
    */
+  @Nullable
   public T get(I input) throws OrmException {
     try {
       return getter.get(input);
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 2d7e31e..f60c08f 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.index;
 
+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;
@@ -21,7 +22,6 @@
 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;
 
 /**
@@ -95,7 +95,7 @@
    */
   default Optional<V> get(K key, QueryOptions opts) throws IOException {
     opts = opts.withStart(0).withLimit(2);
-    List<V> results;
+    ImmutableList<V> results;
     try {
       results = getSource(keyPredicate(key), opts).read().toList();
     } catch (QueryParseException e) {
@@ -120,7 +120,7 @@
    */
   default Optional<FieldBundle> getRaw(K key, QueryOptions opts) throws IOException {
     opts = opts.withStart(0).withLimit(2);
-    List<FieldBundle> results;
+    ImmutableList<FieldBundle> results;
     try {
       results = getSource(keyPredicate(key), opts).readRaw().toList();
     } catch (QueryParseException e) {
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..2b3c63e 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -16,7 +16,6 @@
 
 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;
@@ -176,24 +175,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 (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));
               }
             })
         .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/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index d1e1c30..649dc32 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -21,10 +21,8 @@
 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 java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
diff --git a/java/com/google/gerrit/index/query/DataSource.java b/java/com/google/gerrit/index/query/DataSource.java
index 88cc0e3c..a82337f 100644
--- a/java/com/google/gerrit/index/query/DataSource.java
+++ b/java/com/google/gerrit/index/query/DataSource.java
@@ -15,7 +15,6 @@
 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()}. */
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..8df46a7
--- /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.index.Index;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gwtorm.server.OrmException;
+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/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index 3a4b372..ca5cc9b 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -17,6 +17,7 @@
 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.index.FieldDef;
@@ -27,6 +28,7 @@
 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,32 +55,46 @@
     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) throws OrmException {
+    return queryResults(p).entities();
+  }
+
+  final QueryResult<T> queryResults(Predicate<T> p) throws OrmException {
     try {
-      return queryProcessor.query(p).entities();
+      return queryProcessor.query(p);
     } catch (QueryParseException e) {
       throw new OrmException(e);
     }
@@ -94,7 +110,7 @@
    * @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) throws OrmException {
     try {
       return Lists.transform(queryProcessor.query(queries), QueryResult::entities);
     } catch (QueryParseException e) {
@@ -102,8 +118,52 @@
     }
   }
 
-  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.
+   * @throws OrmException if an error occurred.
+   */
+  protected static <T> ImmutableList<T> queryExhaustively(
+      Supplier<? extends InternalQuery<T, ?>> querySupplier, Predicate<T> predicate)
+      throws OrmException {
+    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/Paginated.java b/java/com/google/gerrit/index/query/Paginated.java
index 20f65dc..c11d8c7 100644
--- a/java/com/google/gerrit/index/query/Paginated.java
+++ b/java/com/google/gerrit/index/query/Paginated.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.QueryOptions;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
 public interface Paginated<T> {
   QueryOptions getOptions();
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
index c6c39c3..12d1dd6 100644
--- a/java/com/google/gerrit/index/query/QueryBuilder.java
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -101,7 +101,7 @@
               && (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));
+              opFactories.put(name, new ReflectionFactory<>(name, method));
             }
           }
         }
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 64d8742..c68f674 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -30,7 +30,6 @@
 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;
@@ -40,7 +39,6 @@
 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 +89,7 @@
 
   private boolean enforceVisibility = true;
   private int userProvidedLimit;
+  private boolean isNoLimit;
   private Set<String> requestedFields;
 
   protected QueryProcessor(
@@ -157,6 +156,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;
@@ -268,7 +272,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())));
@@ -325,7 +329,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 +358,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/json/BUILD b/java/com/google/gerrit/json/BUILD
new file mode 100644
index 0000000..7282dc4
--- /dev/null
+++ b/java/com/google/gerrit/json/BUILD
@@ -0,0 +1,8 @@
+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/BUILD b/java/com/google/gerrit/launcher/BUILD
index 18dcd52..918a83f 100644
--- a/java/com/google/gerrit/launcher/BUILD
+++ b/java/com/google/gerrit/launcher/BUILD
@@ -8,7 +8,7 @@
 )
 
 # The root of the workspace is non-hermetic, but we need it for
-# on-the-fly GWT recompiles and PolyGerrit updates.
+# on-the-flyPolyGerrit updates.
 genrule(
     name = "gen_root",
     outs = ["workspace-root.txt"],
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index e407d08..dec077a 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -94,7 +94,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");
 
@@ -325,7 +324,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..ae36b48 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;
@@ -38,18 +39,15 @@
 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;
@@ -503,32 +501,16 @@
         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);
       } finally {
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 9c6ba74..3b18f2c 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -31,11 +31,13 @@
         "//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/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 9944c8a..0e34c93 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;
@@ -42,11 +41,16 @@
 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 +62,22 @@
 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 +131,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 +144,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 +155,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;
 
@@ -207,7 +207,7 @@
     // 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();
@@ -334,7 +334,8 @@
       } catch (IOException e) {
         throw new OrmException(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 +343,7 @@
         }
 
         @Override
-        public List<FieldBundle> toList() {
+        public ImmutableList<FieldBundle> toList() {
           return fieldBundles;
         }
 
@@ -402,15 +403,16 @@
     }
 
     @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);
@@ -446,15 +448,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 +504,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 +518,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 +635,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/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/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/OperatingSystemMXBeanProvider.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
index 10d589a..35c147e 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
@@ -51,10 +51,9 @@
   private OperatingSystemMXBeanProvider(OperatingSystemMXBean sys)
       throws ReflectiveOperationException {
     this.sys = sys;
-    getProcessCpuTime = sys.getClass().getMethod("getProcessCpuTime", new Class<?>[] {});
+    getProcessCpuTime = sys.getClass().getMethod("getProcessCpuTime");
     getProcessCpuTime.setAccessible(true);
-    getOpenFileDescriptorCount =
-        sys.getClass().getMethod("getOpenFileDescriptorCount", new Class<?>[] {});
+    getOpenFileDescriptorCount = sys.getClass().getMethod("getOpenFileDescriptorCount");
     getOpenFileDescriptorCount.setAccessible(true);
   }
 
diff --git a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 6b9176b..7b34f50 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/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index b08842e..e2fd7f3 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;
@@ -89,18 +87,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;
@@ -123,7 +117,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;
@@ -132,7 +125,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 {
@@ -181,15 +173,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;
@@ -249,12 +232,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");
@@ -293,8 +271,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 {
@@ -343,7 +319,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));
@@ -409,7 +385,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());
 
@@ -420,10 +396,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());
@@ -449,9 +422,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());
@@ -485,8 +456,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)
@@ -502,9 +472,6 @@
       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());
@@ -512,10 +479,6 @@
         ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
   }
 
-  private boolean migrateToNoteDb() {
-    return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(requireNonNull(config));
-  }
-
   private Module createIndexModule() {
     if (luceneModule != null) {
       return luceneModule;
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..14a0b5d 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -15,7 +15,6 @@
 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.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -29,7 +28,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.gerrit.server.schema.NoteDbSchemaVersionCheck;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -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(
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/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..cbc9c3b 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;
@@ -81,10 +79,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);
@@ -163,7 +160,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
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 b7ec2be..dab9d7e 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 2eea88d..bd7d998 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
@@ -67,6 +67,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 7e04e5a..3c6df14 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -21,7 +21,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",
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index deaf139..c1fd1df 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;
 
@@ -35,31 +33,24 @@
 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;
   }
@@ -256,15 +232,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);
@@ -358,8 +333,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
@@ -367,80 +341,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();
-            }
+      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();
       }
     }
   }
@@ -460,7 +404,7 @@
               bind(InitFlags.class).toInstance(init.flags);
             }
           });
-      Injector dbInjector = createDbInjector(SINGLE_USER);
+      Injector dbInjector = createDbInjector();
       switch (IndexModule.getIndexType(dbInjector)) {
         case LUCENE:
           modules.add(new LuceneIndexModuleOnInit());
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/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 8e06aa1..584d8af 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -25,7 +25,6 @@
 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..f19cf39 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -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 1a86106..6b4c7ca 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..f2fc001 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -26,11 +26,9 @@
 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
@@ -43,12 +41,7 @@
 
     // 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/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/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..aa9fca2 100644
--- a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
@@ -15,11 +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.gerrit.server.notedb.Sequences;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -35,16 +34,14 @@
     this.allUsersName = allUsersName;
   }
 
-  public int nextAccountId(ReviewDb db) throws OrmException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed accountSeed = db::nextAccountId;
+  public int nextAccountId() throws OrmException {
     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 7fe3bfa..defc4d3 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -20,7 +20,6 @@
         "//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..b700835 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;
@@ -73,42 +73,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 +107,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 +133,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 +151,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());
@@ -193,8 +171,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 1338efb..de8238d 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -15,57 +15,33 @@
 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.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,8 +50,6 @@
     sitePath = Paths.get(path);
   }
 
-  protected Provider<DataSource> dsProvider;
-
   private Path sitePath = Paths.get(".");
 
   protected SiteProgram() {}
@@ -84,11 +58,6 @@
     this.sitePath = sitePath;
   }
 
-  protected SiteProgram(Path sitePath, Provider<DataSource> dsProvider) {
-    this.sitePath = sitePath;
-    this.dsProvider = dsProvider;
-  }
-
   /** @return the site path specified on the command line. */
   protected Path getSitePath() {
     return sitePath;
@@ -102,12 +71,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 +103,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,33 +113,9 @@
           }
         });
     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);
@@ -198,19 +123,6 @@
       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 +147,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 366a1a0..fbceda6 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -1,22 +1,3 @@
-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",
     srcs = glob(["common/**/*.java"]),
@@ -25,7 +6,6 @@
         "//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 48185d6..4f05bf6 100644
--- a/java/com/google/gerrit/proto/BUILD
+++ b/java/com/google/gerrit/proto/BUILD
@@ -1,14 +1,8 @@
-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..48115ff
--- /dev/null
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -0,0 +1,13 @@
+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..a5cf3ca
--- /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.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 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();
+    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/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index f038a23..74c3403 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -1,28 +1,16 @@
-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",
         "//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..47c9b40 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,10 @@
  * </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.
-   */
+  /** 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 +70,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 +139,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 +147,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..10e302b 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,16 @@
    * 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);
   }
 
   /** 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() {}
@@ -62,7 +60,6 @@
   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() {}
@@ -120,7 +117,6 @@
   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 +144,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 +169,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..4ca609e 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
 import java.util.Objects;
 
@@ -23,10 +22,8 @@
   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() {
@@ -58,7 +55,6 @@
     }
   }
 
-  @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..3f8e4d4 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.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.Objects;
@@ -24,13 +24,10 @@
   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() {
@@ -75,17 +72,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..6707abb 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
 import java.util.Objects;
 
@@ -23,10 +22,8 @@
   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() {
@@ -54,7 +51,6 @@
     }
   }
 
-  @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..3b87115 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.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.Objects;
@@ -24,13 +24,10 @@
   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() {
@@ -75,17 +72,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..18ab2cf 100644
--- a/java/com/google/gerrit/reviewdb/client/Branch.java
+++ b/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -14,7 +14,6 @@
 
 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}. */
@@ -23,10 +22,8 @@
   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() {
diff --git a/java/com/google/gerrit/reviewdb/client/Change.java b/java/com/google/gerrit/reviewdb/client/Change.java
index 8d4de05..c1443f2 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;
@@ -97,7 +96,6 @@
   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() {}
@@ -260,7 +258,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 +425,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 +441,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 +475,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 +533,6 @@
     isPrivate = other.isPrivate;
     workInProgress = other.workInProgress;
     reviewStarted = other.reviewStarted;
-    noteDbState = other.noteDbState;
     revertOf = other.revertOf;
   }
 
@@ -698,6 +671,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 +727,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..ef05dd6 100644
--- a/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/java/com/google/gerrit/reviewdb/client/ChangeMessage.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.StringKey;
 import java.sql.Timestamp;
 import java.util.Objects;
@@ -24,10 +24,8 @@
   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() {
@@ -55,32 +53,25 @@
     }
   }
 
-  @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..7b4f4c6 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<?>> {
@@ -26,7 +25,6 @@
     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..5dbae55 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}. */
@@ -39,10 +38,8 @@
   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() {
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..a2c7010 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));
@@ -77,10 +77,8 @@
   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() {
@@ -156,21 +154,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 +172,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 +185,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..82a720f 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;
@@ -25,13 +25,10 @@
   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() {
@@ -65,7 +62,6 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   /**
@@ -84,20 +80,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..6e0e5c9 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;
@@ -35,7 +34,6 @@
   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 +95,6 @@
 
   protected String localDefaultDashboardId;
 
-  protected String themeName;
-
   protected String configRefState;
 
   protected Project() {}
@@ -184,22 +180,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
index 2f6008f..57d786b 100644
--- a/java/com/google/gerrit/reviewdb/client/TrackingId.java
+++ b/java/com/google/gerrit/reviewdb/client/TrackingId.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
 import com.google.gwtorm.client.StringKey;
 
@@ -27,7 +26,6 @@
   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() {}
@@ -51,7 +49,6 @@
   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() {}
@@ -74,13 +71,10 @@
   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() {
@@ -114,7 +108,6 @@
     }
   }
 
-  @Column(id = 1, name = Column.NONE)
   protected Key key;
 
   protected TrackingId() {}
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..45588a3 100644
--- a/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/ApprovalCopier.java
@@ -26,11 +26,9 @@
 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;
@@ -74,78 +72,27 @@
     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)
+  Iterable<PatchSetApproval> getForPatchSet(
+      ChangeNotes notes, PatchSet.Id psId, @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));
-    }
+    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);
+    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,
@@ -153,7 +100,7 @@
       Iterable<PatchSetApproval> dontCopy)
       throws OrmException {
     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();
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 3625de6..1b5983a 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -17,15 +17,12 @@
 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;
@@ -41,10 +38,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;
@@ -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,11 @@
   /**
    * 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) throws OrmException {
     return notes.load().getReviewers();
   }
 
@@ -148,38 +127,29 @@
    */
   public ReviewerSet getReviewers(ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
       throws OrmException {
-    if (!migration.readChanges()) {
-      return ReviewerSet.fromApprovals(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();
-    }
     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,7 +161,6 @@
   }
 
   public List<PatchSetApproval> addReviewers(
-      ReviewDb db,
       ChangeNotes notes,
       ChangeUpdate update,
       LabelTypes labelTypes,
@@ -200,13 +169,7 @@
       throws OrmException {
     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 +179,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 +190,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 +218,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;
@@ -308,9 +264,8 @@
   }
 
   /**
-   * 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.
@@ -320,7 +275,6 @@
    * @throws OrmException
    */
   public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(
-      ReviewDb db,
       ChangeUpdate update,
       LabelTypes labelTypes,
       PatchSet ps,
@@ -336,7 +290,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 +300,6 @@
     for (PatchSetApproval psa : cells) {
       update.putApproval(psa.getLabel(), psa.getValue());
     }
-    db.patchSetApprovals().insert(cells);
     return cells;
   }
 
@@ -377,53 +330,34 @@
     }
   }
 
-  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ReviewDb db, ChangeNotes notes)
+  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(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();
-    }
     return notes.load().getApprovals();
   }
 
   public Iterable<PatchSetApproval> byPatchSet(
-      ReviewDb db,
-      ChangeNotes notes,
-      PatchSet.Id psId,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
+      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);
+    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);
+    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));
+      return getSubmitter(c, byChange(notes).get(c));
     } catch (OrmException e) {
       return null;
     }
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 6ce3be4..2634048 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -34,13 +34,16 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//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/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",
@@ -83,7 +86,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 969cf38..8b9a6f4a 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -15,37 +15,24 @@
 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 +87,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) throws OrmException {
     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,74 +99,24 @@
         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 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));
-    }
-
+  // TODO(xchangcheng): Reconsider implementation now that there is only a single ID.
+  public void replaceChangeMessage(ChangeUpdate update, int targetMessageIdx, String newMessage) {
     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);
-      }
-    }
-  }
-
   /**
    * @param tag value of a tag, or null.
    * @return whether the tag starts with the autogenerated prefix.
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..39df152 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -16,16 +16,12 @@
 
 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.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -35,31 +31,23 @@
 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;
@@ -70,12 +58,7 @@
 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 +110,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;
   }
 
@@ -158,7 +136,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 +179,63 @@
     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) throws OrmException {
+    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)
+      throws OrmException {
+    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) throws OrmException {
     notes.load();
     return sort(Lists.newArrayList(notes.getComments().values()));
   }
 
   public List<RobotComment> robotCommentsByChange(ChangeNotes notes) throws OrmException {
-    if (!migration.readChanges()) {
-      return ImmutableList.of();
-    }
-
     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) throws OrmException {
     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) throws OrmException {
     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) throws OrmException {
     return commentsOnFile(notes.load().getComments().values(), file);
   }
 
-  public List<Comment> publishedByPatchSet(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
+  public List<Comment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId)
       throws OrmException {
-    if (!migration.readChanges()) {
-      return removeCommentsOnAncestorOfCommitMessage(
-          sort(toComments(serverId, db.patchComments().publishedByPatchSet(psId).toList())));
-    }
     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();
-    }
     return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
   }
 
@@ -329,49 +252,28 @@
         .collect(toList());
   }
 
-  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()));
-    }
+  public List<Comment> draftByPatchSetAuthor(PatchSet.Id psId, Account.Id author, ChangeNotes notes)
+      throws OrmException {
     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)
+      throws OrmException {
     return commentsOnFile(notes.load().getDraftComments(author).values(), file);
   }
 
-  public List<Comment> draftByChangeAuthor(ReviewDb db, ChangeNotes notes, Account.Id author)
+  public List<Comment> draftByChangeAuthor(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());
-    }
     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,37 +282,14 @@
     }
   }
 
-  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);
   }
 
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index cd7cd47..e6c46df 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..44d3493 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 {}
 
@@ -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/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..e7f1d2a 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,11 +28,9 @@
 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;
@@ -47,7 +39,6 @@
 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 +49,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) throws OrmException {
+    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) throws OrmException {
     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) throws OrmException {
     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) throws OrmException {
     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) throws OrmException {
     return ImmutableMap.copyOf(Maps.filterKeys(notes.load().getPatchSets(), patchSetIds::contains));
   }
 
   public PatchSet insert(
-      ReviewDb db,
       RevWalk rw,
       ChangeUpdate update,
       PatchSet.Id psId,
@@ -129,10 +92,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 +107,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,11 +128,9 @@
     }
   }
 
-  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. */
@@ -192,7 +145,7 @@
   /** Is the current patch set locked against state changes? */
   public boolean isPatchSetLocked(ChangeNotes notes) throws OrmException, IOException {
     Change change = notes.getChange();
-    if (change.getStatus() == Change.Status.MERGED) {
+    if (change.isMerged()) {
       return false;
     }
 
@@ -202,7 +155,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..75a364c 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.Nullable;
 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;
@@ -55,9 +54,8 @@
       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) {
@@ -74,7 +72,7 @@
         throw new OrmException(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 4e349e6..e6d6be1 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -35,7 +35,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.change.ChangeResource;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -167,7 +166,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;
@@ -177,14 +175,12 @@
       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;
@@ -229,7 +225,7 @@
         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(
@@ -259,7 +255,7 @@
                   changeId.get(), command.getRefName(), command.getResult()));
         }
       }
-      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);
     }
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 b564157..0000000
--- a/java/com/google/gerrit/server/UsedAt.java
+++ /dev/null
@@ -1,45 +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,
-    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/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index b43e786..f17f387 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -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 27945d1..d97ff7b 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -30,7 +30,6 @@
 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,6 +39,7 @@
 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;
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 8c7cdb4..14f4a38 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,598 @@
 
 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, OrmException;
+
+    Stream<AccountState> search(I input) throws OrmException, IOException, ConfigInvalidException;
+
+    boolean shortCircuitIfNoResults();
+
+    default Optional<Stream<AccountState>> trySearch(String input)
+        throws OrmException, 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 OrmException, 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 OrmException, 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) throws OrmException {
+      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) throws OrmException {
+      // 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 OrmException if an error occurs.
+   * @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 OrmException, ConfigInvalidException, IOException {
+    return searchImpl(input, searchers, visibilitySupplier());
+  }
 
-    Account match = null;
-    for (Account.Id id : r) {
-      Optional<Account> account = byId.get(id).map(AccountState::getAccount);
-      if (!account.map(Account::isActive).orElse(false)) {
+  /**
+   * 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 OrmException if an error occurs.
+   * @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 OrmException, ConfigInvalidException, IOException {
+    return searchImpl(input, nameOrEmailSearchers, visibilitySupplier());
+  }
+
+  private Supplier<Predicate<AccountState>> visibilitySupplier() {
+    return () -> accountControlFactory.get()::canSee;
+  }
+
+  @VisibleForTesting
+  Result searchImpl(
+      String input,
+      ImmutableList<Searcher<?>> searchers,
+      Supplier<Predicate<AccountState>> visibilitySupplier)
+      throws OrmException, 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 -> (a.getAccount().isActive() ? 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..b862793 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.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());
   }
 
   /**
@@ -458,7 +491,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) {
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/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..f33d8fe 100644
--- a/java/com/google/gerrit/server/account/Preferences.java
+++ b/java/com/google/gerrit/server/account/Preferences.java
@@ -50,7 +50,6 @@
 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;
@@ -574,7 +573,7 @@
       }
 
       int i = 1;
-      for (Entry<String, String> e : urlAliases.entrySet()) {
+      for (Map.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++;
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/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index 910ecd3..d4f11f0 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -12,6 +12,8 @@
         "//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",
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 4807b94..6763f51 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -16,6 +16,8 @@
 
 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.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -44,18 +46,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 +72,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 +102,18 @@
 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.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
 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);
@@ -754,4 +677,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/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/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/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..721d878 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -117,9 +117,6 @@
       case CODE:
         type = FilterType.CODE;
         break;
-      case PARENT_CANDIDATES:
-        type = FilterType.PARENT_CANDIDATES;
-        break;
       case PERMISSIONS:
         type = FilterType.PERMISSIONS;
         break;
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 2b66334..addb7f9 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 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;
@@ -60,10 +61,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:
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 223b112..f33a4ed 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 1756713..f66c129 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -55,7 +55,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
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 a53a8c2..888a554 100644
--- a/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -186,13 +186,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);
@@ -402,7 +396,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/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index ad1d396..f76b8db 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -112,11 +112,6 @@
   }
 
   private static <K, V> Weigher<K, V> unitWeight() {
-    return new Weigher<K, V>() {
-      @Override
-      public int weigh(K key, V value) {
-        return 1;
-      }
-    };
+    return (key, value) -> 1;
   }
 }
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index 957a153..cd9912c 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -4,6 +4,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/proto",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:protobuf",
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/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index f87c006..16cbe17 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -5,10 +5,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..458a680 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;
@@ -49,8 +45,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 +53,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 +63,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 +71,6 @@
 
     this.accountState = accountState;
     this.msgTxt = Strings.nullToEmpty(msgTxt);
-    this.notifyHandling = notifyHandling;
-    this.accountsToNotify = accountsToNotify;
   }
 
   @Nullable
@@ -96,16 +83,16 @@
     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;
   }
 
@@ -122,18 +109,18 @@
 
   @Override
   public void postUpdate(Context ctx) throws OrmException {
+    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/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 0a9fe81..52c0451 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -29,7 +29,6 @@
 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;
@@ -180,8 +179,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..46d57b5 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,7 +39,6 @@
 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;
@@ -70,16 +65,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 +105,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 +116,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 +129,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 +139,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) {
@@ -173,14 +161,13 @@
       throws RestApiException, OrmException, 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 +178,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 +200,7 @@
     checkAdded();
 
     if (patchSet == null) {
-      patchSet = requireNonNull(psUtil.current(ctx.getDb(), ctx.getNotes()));
+      patchSet = requireNonNull(psUtil.current(ctx.getNotes()));
     }
     return true;
   }
@@ -251,16 +238,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..8c67531 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,15 +14,9 @@
 
 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.query.change.ChangeData;
@@ -30,18 +24,15 @@
 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;
 
   @Inject
-  BatchAbandon(Provider<ReviewDb> dbProvider, AbandonOp.Factory abandonOpFactory) {
-    this.dbProvider = dbProvider;
+  BatchAbandon(AbandonOp.Factory abandonOpFactory) {
     this.abandonOpFactory = abandonOpFactory;
   }
 
@@ -58,14 +49,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,9 +64,7 @@
                   "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();
     }
@@ -88,14 +77,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 +86,6 @@
       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());
   }
 }
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..5e7a9bf 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -30,7 +30,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;
@@ -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(
@@ -203,7 +199,7 @@
     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) {
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 80d05df..5371908 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;
@@ -124,8 +120,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 +250,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(
@@ -389,7 +372,6 @@
       throws RestApiException, OrmException, 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 +394,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 +407,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 +416,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 +433,7 @@
               patchSet.getCreatedOn(),
               message,
               ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
-      cmUtil.addChangeMessage(db, update, changeMessage);
+      cmUtil.addChangeMessage(update, changeMessage);
     }
     return true;
   }
@@ -467,7 +441,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 +453,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 05cd235..fdb51c4 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -74,7 +74,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 +83,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,7 +96,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;
@@ -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;
 
@@ -268,11 +269,11 @@
   }
 
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
-    return format(changeDataFactory.create(db.get(), rsrc.getNotes()));
+    return format(changeDataFactory.create(rsrc.getNotes()));
   }
 
   public ChangeInfo format(Change change) throws OrmException {
-    return format(changeDataFactory.create(db.get(), change));
+    return format(changeDataFactory.create(change));
   }
 
   public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException {
@@ -284,7 +285,7 @@
   }
 
   public ChangeInfo format(RevisionResource rsrc) throws OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
   }
 
@@ -323,15 +324,14 @@
       Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) throws OrmException {
     ChangeNotes notes;
     try {
-      notes = notesFactory.createChecked(db.get(), project, id);
+      notes = notesFactory.createChecked(project, id);
     } catch (OrmException 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) {
@@ -496,7 +496,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;
         }
       }
@@ -509,12 +509,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)) {
@@ -535,6 +535,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()) {
@@ -545,7 +546,7 @@
       }
     }
 
-    if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
+    if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
       out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
     }
 
@@ -558,7 +559,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();
       }
@@ -665,7 +666,7 @@
   }
 
   private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException {
-    List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes());
+    List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
     if (messages.isEmpty()) {
       return Collections.emptyList();
     }
@@ -800,7 +801,7 @@
    */
   private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
       throws OrmException {
-    PermissionBackend.WithUser withUser = permissionBackend.user(user).database(db);
+    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 a6786d8..170223d 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -24,16 +24,15 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 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;
@@ -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()),
@@ -351,8 +348,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
@@ -407,7 +404,6 @@
 
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      ReviewDb db,
       Change change,
       PatchSet patch,
       ChangeData.Factory changeDataFactory,
@@ -421,7 +417,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..3ec61ef 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -29,7 +29,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;
@@ -42,7 +41,6 @@
 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,7 +158,7 @@
       // 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());
+      accounts.addAll(approvalUtil.getReviewers(notes).all());
     } catch (OrmException e) {
       // This ETag will be invalidated if it loads next time.
     }
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 128297f..83c7387 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;
@@ -30,15 +31,12 @@
 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.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,7 +50,6 @@
 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;
@@ -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,7 +227,7 @@
 
   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()));
@@ -259,7 +255,7 @@
     List<PatchSet> all;
     try {
       // Iterate in descending order.
-      all = PS_ID_ORDER.sortedCopy(psUtil.byChange(db.get(), notes));
+      all = PS_ID_ORDER.sortedCopy(psUtil.byChange(notes));
     } catch (OrmException e) {
       return error("Failed to look up patch sets", e);
     }
@@ -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,9 +421,7 @@
         }
         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;
           }
@@ -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,18 +542,18 @@
           }
         }
 
+        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) {
@@ -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) {
@@ -667,15 +675,9 @@
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, 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));
 
-      // 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;
@@ -708,11 +710,11 @@
       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 +722,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..1a579f3 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -14,26 +14,22 @@
 
 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 +38,42 @@
 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());
+      throws RestApiException, OrmException, 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);
 
     ctx.deleteChange();
     changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
@@ -84,8 +82,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,20 +106,6 @@
     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) {
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index ad4f4af..d7d4c0e 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;
@@ -35,27 +33,20 @@
   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
@@ -78,24 +69,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..495ee84 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.common.errors.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,13 +37,10 @@
 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;
@@ -72,8 +68,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 +89,6 @@
       ReviewerDeleted reviewerDeleted,
       Provider<IdentifiedUser> user,
       DeleteReviewerSender.Factory deleteReviewerSenderFactory,
-      NotesMigration migration,
-      NotifyUtil notifyUtil,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
       @Assisted AccountState reviewerAccount,
@@ -108,8 +100,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;
@@ -124,11 +114,11 @@
     // 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 +154,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 +190,14 @@
         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();
     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 +209,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..01ac37b 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;
@@ -55,7 +53,6 @@
 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 +76,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 +83,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;
@@ -113,7 +107,7 @@
 
     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));
@@ -122,7 +116,7 @@
   /** 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;
+    boolean isMerged = cd.change().isMerged();
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelType> toCheck = new HashMap<>();
     for (SubmitRecord rec : submitRecords(cd)) {
@@ -263,7 +257,6 @@
     Map<String, Short> result = new HashMap<>();
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
-            db.get(),
             lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()),
             cd.change().currentPatchSetId(),
             accountId,
@@ -293,7 +286,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());
@@ -438,20 +432,21 @@
   private void setAllApprovals(
       AccountLoader accountLoader, ChangeData cd, Map<String, LabelWithStatus> labels)
       throws OrmException, PermissionBackendException {
-    Change.Status status = cd.change().getStatus();
     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);
@@ -506,7 +501,7 @@
    */
   private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
       throws OrmException {
-    PermissionBackend.WithUser withUser = permissionBackend.absentUser(user).database(db);
+    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..65da083
--- /dev/null
+++ b/java/com/google/gerrit/server/change/NotifyResolver.java
@@ -0,0 +1,118 @@
+// 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.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 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();
+
+    // TODO(dborowitz): Should be ImmutableSetMultimap.
+    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, OrmException, 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 OrmException, 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..8f6b8f5 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;
@@ -79,7 +73,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 +91,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 +105,6 @@
   public PatchSetInserter(
       PermissionBackend permissionBackend,
       ApprovalsUtil approvalsUtil,
-      ApprovalCopier approvalCopier,
       ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
       CommitValidators.Factory commitValidatorsFactory,
@@ -127,7 +117,6 @@
       @Assisted ObjectId commitId) {
     this.permissionBackend = permissionBackend;
     this.approvalsUtil = approvalsUtil;
-    this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -176,24 +165,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;
   }
 
@@ -218,13 +196,11 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws ResourceConflictException, OrmException, IOException {
-    ReviewDb db = ctx.getDb();
-
     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 +209,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 +239,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 +258,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(
@@ -311,11 +277,7 @@
     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 ddc9661..0135683 100644
--- a/java/com/google/gerrit/server/change/PureRevert.java
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -19,7 +19,6 @@
 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;
@@ -27,7 +26,6 @@
 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;
@@ -49,7 +47,6 @@
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final ChangeNotes.Factory notesFactory;
-  private final Provider<ReviewDb> dbProvider;
   private final PatchSetUtil psUtil;
 
   @Inject
@@ -58,19 +55,17 @@
       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;
   }
 
   public PureRevertInfo get(ChangeNotes notes, @Nullable String claimedOriginal)
       throws OrmException, IOException, BadRequestException, ResourceConflictException {
-    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), notes);
+    PatchSet currentPatchSet = psUtil.current(notes);
     if (currentPatchSet == null) {
       throw new ResourceConflictException("current revision is missing");
     }
@@ -81,9 +76,7 @@
       }
       PatchSet ps =
           psUtil.current(
-              dbProvider.get(),
-              notesFactory.createChecked(
-                  dbProvider.get(), notes.getProjectName(), notes.getChange().getRevertOf()));
+              notesFactory.createChecked(notes.getProjectName(), notes.getChange().getRevertOf()));
       claimedOriginal = ps.getRevision().get();
     }
 
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 1f216f0..e4f4cc3 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;
@@ -69,9 +69,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 +126,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 +136,11 @@
     return this;
   }
 
+  public RebaseChangeOp setSendEmail(boolean sendEmail) {
+    this.sendEmail = sendEmail;
+    return this;
+  }
+
   public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
     this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
     return this;
@@ -178,18 +178,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,8 +200,14 @@
               + " 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);
   }
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 22f98b8..db68898 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -22,10 +22,8 @@
 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;
@@ -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;
   }
 
@@ -89,8 +84,6 @@
   }
 
   public Base parseBase(RevisionResource rsrc, String base) throws OrmException {
-    ReviewDb db = dbProvider.get();
-
     // 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));
       }
     }
 
@@ -131,7 +123,7 @@
     if (rsrc.getChange().getId().equals(id)) {
       return rsrc.getNotes();
     }
-    return notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
+    return notesFactory.createChecked(rsrc.getProject(), id);
   }
 
   /**
@@ -171,12 +163,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..21fc03c 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;
@@ -89,6 +84,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 +147,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 +161,6 @@
       AccountLoader.Factory accountLoaderFactory,
       @GerritServerConfig Config cfg,
       ReviewerJson json,
-      NotesMigration migration,
-      NotifyUtil notifyUtil,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousProvider,
       AddReviewersOp.Factory addReviewersOpFactory,
@@ -179,8 +172,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 +181,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.
@@ -204,42 +194,42 @@
    * @throws ConfigInvalidException
    */
   public ReviewerAddition prepare(
-      ReviewDb db, ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
+      ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
       throws OrmException, 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 +239,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)
+      AddReviewerInput input, ChangeNotes notes, CurrentUser user)
       throws OrmException, 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 +272,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 +317,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 +329,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 +341,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 +361,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 +368,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 +423,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 +437,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;
     }
 
@@ -519,7 +456,7 @@
       // 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,7 +506,6 @@
   }
 
   public ReviewerAdditionList prepare(
-      ReviewDb db,
       ChangeNotes notes,
       CurrentUser user,
       Iterable<? extends AddReviewerInput> inputs,
@@ -586,11 +522,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);
   }
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index ef2c926..fd5772d 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;
@@ -38,7 +37,6 @@
 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 +44,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 +52,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;
@@ -76,7 +71,7 @@
     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()) {
@@ -95,7 +90,7 @@
 
   public List<ReviewerInfo> format(ReviewerResource rsrc)
       throws OrmException, PermissionBackendException {
-    return format(ImmutableList.<ReviewerResource>of(rsrc));
+    return format(ImmutableList.of(rsrc));
   }
 
   public ReviewerInfo format(ReviewerInfo out, Account.Id reviewerAccountId, ChangeData cd)
@@ -105,7 +100,7 @@
         out,
         reviewerAccountId,
         cd,
-        approvalsUtil.byPatchSetUser(db.get(), cd.notes(), psId, reviewerAccountId, null, null));
+        approvalsUtil.byPatchSetUser(cd.notes(), psId, reviewerAccountId, null, null));
   }
 
   public ReviewerInfo format(
@@ -128,8 +123,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..42175eb 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;
@@ -212,7 +211,7 @@
       AccountLoader accountLoader,
       ChangeData cd,
       Map<PatchSet.Id, PatchSet> map,
-      Optional<Id> limitToPsId,
+      Optional<PatchSet.Id> limitToPsId,
       ChangeInfo changeInfo)
       throws PatchListNotAvailableException, GpgException, OrmException, IOException,
           PermissionBackendException {
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..dd24ff6 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -99,7 +99,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,7 +113,7 @@
     }
     ChangeMessage cmsg =
         ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_SET_ASSIGNEE);
-    cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+    cmUtil.addChangeMessage(update, cmsg);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/change/SetHashtagsOp.java b/java/com/google/gerrit/server/change/SetHashtagsOp.java
index d11b2df..4f73053 100644
--- a/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -32,7 +32,6 @@
 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;
@@ -52,7 +51,6 @@
     SetHashtagsOp create(HashtagsInput input);
   }
 
-  private final NotesMigration notesMigration;
   private final ChangeMessagesUtil cmUtil;
   private final PluginSetContext<HashtagValidationListener> validationListeners;
   private final HashtagsEdited hashtagsEdited;
@@ -67,12 +65,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;
@@ -88,9 +84,6 @@
   public boolean updateChange(ChangeContext ctx)
       throws AuthException, BadRequestException, MethodNotAllowedException, OrmException,
           IOException {
-    if (!notesMigration.readChanges()) {
-      throw new MethodNotAllowedException("Cannot add hashtags; NoteDb is disabled");
-    }
     if (input == null || (input.add == null && input.remove == null)) {
       updatedHashtags = ImmutableSortedSet.of();
       return false;
@@ -125,13 +118,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) {
diff --git a/java/com/google/gerrit/server/change/SetPrivateOp.java b/java/com/google/gerrit/server/change/SetPrivateOp.java
index b7f444c..017dbec 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;
@@ -44,7 +46,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 +57,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 +74,21 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException {
+  public boolean updateChange(ChangeContext ctx)
+      throws ResourceConflictException, OrmException, 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 +99,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 +121,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/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 35b4e6e..e5fdd8f 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -14,26 +14,18 @@
 
 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;
@@ -59,42 +51,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");
-    }
-  }
-
   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 +78,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 {
     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 +100,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 +119,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/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 b4f9cc7..e7baa55 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;
@@ -168,8 +169,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;
@@ -221,7 +222,6 @@
     bind(IdGenerator.class);
     bind(RulesCache.class);
     bind(BlameCache.class).to(BlameCacheImpl.class);
-    bind(Sequences.class);
     install(authModule);
     install(AccountCacheImpl.module());
     install(BatchUpdate.module());
@@ -245,7 +245,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());
@@ -253,8 +253,6 @@
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
 
-    bind(AccountResolver.class);
-
     factory(AddReviewerSender.Factory.class);
     factory(DeleteReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
@@ -299,8 +297,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);
@@ -313,6 +311,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);
@@ -394,6 +393,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);
@@ -404,9 +404,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/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..9698867 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;
@@ -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();
@@ -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(
@@ -401,20 +398,15 @@
     }
 
     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);
@@ -457,7 +449,7 @@
   }
 
   private PatchSet lookupCurrentPatchSet(ChangeNotes notes) throws OrmException {
-    return patchSetUtil.current(reviewDb.get(), notes);
+    return patchSetUtil.current(notes);
   }
 
   private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
@@ -543,7 +535,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 +552,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 +619,7 @@
     return user.newRefLogIdent(timestamp, tz);
   }
 
-  private void reindex(Change change) throws IOException, OrmException {
-    indexer.index(reviewDb.get(), change);
+  private void reindex(Change change) throws IOException {
+    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..898f427 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,23 +16,19 @@
 
 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.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;
@@ -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,7 +145,6 @@
    * @param edit change edit to publish
    * @param notify Notify handling that defines to whom email notifications should be sent after the
    *     change edit is published.
-   * @param accountsToNotify Accounts that should be notified after the change edit is published.
    * @throws IOException
    * @throws OrmException
    * @throws UpdateException
@@ -162,9 +154,8 @@
       BatchUpdate.Factory updateFactory,
       ChangeNotes notes,
       CurrentUser user,
-      final ChangeEdit edit,
-      NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
+      ChangeEdit edit,
+      NotifyResolver.Result notify)
       throws IOException, OrmException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
@@ -178,11 +169,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 +188,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(),
@@ -230,7 +217,7 @@
     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,8 +225,7 @@
       int pos = ref.getName().lastIndexOf('/');
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
-      return psUtil.get(
-          db.get(), notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
+      return psUtil.get(notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
     } catch (OrmException | 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..03b5d54 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;
@@ -37,7 +36,6 @@
 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,22 +62,18 @@
 
   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
@@ -111,7 +105,7 @@
   protected void fireEvent(Change change, ChangeEvent event)
       throws OrmException, 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 +115,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 +126,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));
       }
@@ -142,7 +136,7 @@
 
   protected void fireEvent(Event event) throws OrmException, 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));
       }
@@ -173,12 +167,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) {
@@ -209,10 +201,7 @@
       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/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 2550efd..7904b28 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -35,7 +35,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;
@@ -65,7 +64,6 @@
 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) throws OrmException {
+    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) throws OrmException {
+    Collection<Account.Id> reviewers = approvalsUtil.getReviewers(notes).all();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
@@ -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,7 +493,7 @@
           p.sizeInsertions += pe.getInsertions();
         }
       }
-      p.kind = changeKindCache.getChangeKind(db, change, patchSet);
+      p.kind = changeKindCache.getChangeKind(change, patchSet);
     } catch (IOException | OrmException e) {
       logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
     } catch (PatchListObjectTooLargeException e) {
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 972266a..750b579 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -45,13 +45,11 @@
 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;
@@ -60,13 +58,11 @@
 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;
@@ -151,50 +144,44 @@
   }
 
   private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info) throws OrmException {
-    return psUtil.get(db.get(), notes, PatchSet.Id.fromRef(info.ref));
+    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 (OrmException 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;
         });
   }
 
@@ -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();
@@ -332,7 +316,7 @@
       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));
       for (AccountInfo reviewer : ev.getReviewers()) {
         event.reviewer = accountAttributeSupplier(reviewer);
         dispatcher.run(d -> d.postEvent(event));
@@ -379,15 +363,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));
   }
 
@@ -420,7 +400,7 @@
 
       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));
@@ -438,7 +418,7 @@
 
       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));
@@ -456,7 +436,7 @@
 
       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));
@@ -509,7 +489,7 @@
       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());
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 485ed50..30450dd 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;
@@ -35,7 +34,6 @@
 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 +61,6 @@
   }
 
   private final ChangeData.Factory changeDataFactory;
-  private final Provider<ReviewDb> db;
   private final ChangeJson.Factory changeJsonFactory;
   private final RevisionJson.Factory revisionJsonFactory;
 
@@ -71,10 +68,8 @@
   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;
   }
@@ -92,7 +87,7 @@
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
       throws OrmException, PatchListNotAvailableException, GpgException, IOException,
           PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(db.get(), project, ps.getId().getParentKey());
+    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/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/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index e043e9f..3fd69a2 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -24,6 +24,7 @@
 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;
@@ -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,7 +80,7 @@
               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());
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 4991715..3cb771e 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 6a7822e..fc9abb4 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]";
 
   private 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..3f037d2 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -30,7 +30,6 @@
 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;
@@ -76,10 +75,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());
   }
@@ -106,33 +101,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 +132,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;
         });
   }
 
diff --git a/java/com/google/gerrit/server/git/HookUtil.java b/java/com/google/gerrit/server/git/HookUtil.java
index 42d3f69..6e8f27c 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;
 
@@ -38,7 +39,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 f964916..5d7a27b 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.common.data.LabelType;
 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 +43,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;
@@ -59,7 +58,6 @@
 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;
@@ -347,7 +350,7 @@
     for (Map.Entry<String, MergeResult<? extends Sequence>> entry : mergeResults.entrySet()) {
       MergeResult<? extends Sequence> p = entry.getValue();
       try (TemporaryBuffer buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
-        fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8.name());
+        fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
         buf.close();
 
         try (InputStream in = buf.openInputStream()) {
@@ -582,7 +585,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));
   }
@@ -597,7 +600,7 @@
 
   private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), notes, psId, null, null);
+      return approvalsUtil.byPatchSet(notes, psId, null, null);
     } catch (OrmException e) {
       logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
       return Collections.emptyList();
@@ -630,7 +633,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;
@@ -728,7 +731,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 b3a1d72..55bb222a 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -40,7 +40,6 @@
 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;
@@ -120,14 +119,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);
@@ -151,13 +149,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;
   }
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/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/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index a7336f0..d455b82 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 8b14177..b33aa3c 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -19,8 +19,8 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
 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 java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -462,7 +462,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..e0eb3f2 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -18,6 +18,7 @@
 
 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 +32,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;
@@ -175,29 +175,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(
@@ -341,20 +357,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/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index bf3d270..9bad21d 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;
@@ -78,7 +78,9 @@
     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) {
diff --git a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
index 7adb21b..0f081be 100644
--- a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
+++ b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.receive;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import java.util.Collection;
 import org.eclipse.jgit.transport.PostReceiveHook;
@@ -22,17 +22,15 @@
 import org.eclipse.jgit.transport.ReceivePack;
 
 class LazyPostReceiveHookChain implements PostReceiveHook {
-  private final DynamicSet<PostReceiveHook> hooks;
+  private final PluginSetContext<PostReceiveHook> hooks;
 
   @Inject
-  LazyPostReceiveHookChain(DynamicSet<PostReceiveHook> hooks) {
+  LazyPostReceiveHookChain(PluginSetContext<PostReceiveHook> hooks) {
     this.hooks = hooks;
   }
 
   @Override
   public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
-    for (PostReceiveHook h : hooks) {
-      h.onPostReceive(rp, commands);
-    }
+    hooks.runEach(h -> h.onPostReceive(rp, commands));
   }
 }
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 0abaed4..103edd8 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;
@@ -86,16 +87,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 +112,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 +121,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;
@@ -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) {
@@ -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:");
@@ -1317,7 +1366,6 @@
     boolean deprecatedTopicSeen;
     final ReceiveCommand cmd;
     final LabelTypes labelTypes;
-    final NotesMigration notesMigration;
     private final boolean defaultPublishComments;
     Branch.NameKey dest;
     PermissionBackend.ForRef perm;
@@ -1345,8 +1393,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;
 
@@ -1389,7 +1435,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",
@@ -1465,10 +1511,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);
@@ -1476,17 +1519,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
@@ -1528,15 +1565,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;
@@ -1596,19 +1624,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;
@@ -1625,7 +1654,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);
@@ -1658,6 +1687,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)) {
@@ -1741,9 +1775,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");
@@ -1781,12 +1816,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(
@@ -1804,6 +1848,7 @@
 
     if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
       this.magicBranch = magicBranch;
+      this.resultChangeIds.setMagicPush(true);
     }
   }
 
@@ -1851,17 +1896,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());
@@ -1887,7 +1933,7 @@
 
     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");
@@ -1927,7 +1973,7 @@
    */
   private boolean requestReplace(
       ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) {
-    if (change.getStatus().isClosed()) {
+    if (change.isClosed()) {
       reject(
           cmd,
           changeFormatter.changeClosed(
@@ -1967,7 +2013,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);
@@ -2231,7 +2277,7 @@
   private boolean foundInExistingRef(Collection<Ref> existingRefs) throws OrmException {
     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());
@@ -2435,14 +2481,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));
@@ -2501,7 +2546,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);
     }
   }
 
@@ -2540,7 +2585,7 @@
   private void readChangesForReplace() throws OrmException {
     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;
     }
@@ -2552,6 +2597,7 @@
     final ObjectId newCommitId;
     final ReceiveCommand inputCommand;
     final boolean checkMergedInto;
+    RevCommit revCommit;
     ChangeNotes notes;
     BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet.Id psId;
@@ -2569,6 +2615,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 {
@@ -2653,13 +2704,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)) {
@@ -2713,10 +2764,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);
@@ -2856,7 +2910,7 @@
           new BatchUpdateOp() {
             @Override
             public boolean updateChange(ChangeContext ctx) throws OrmException {
-              PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+              PatchSet ps = psUtil.get(ctx.getNotes(), psId);
               List<String> oldGroups = ps.getGroups();
               if (oldGroups == null) {
                 if (groups == null) {
@@ -2865,7 +2919,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;
             }
           });
@@ -3085,11 +3139,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());
@@ -3116,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(requestScopePropagator, psId, refName));
@@ -3148,6 +3203,7 @@
                   continue;
                 }
                 req.addOps(bu, null);
+                bu.addOp(id, setPrivateOpFactory.create(false, null));
                 bu.addOp(
                     id,
                     mergedByPushOpFactory
@@ -3169,7 +3225,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;
           },
@@ -3187,7 +3243,7 @@
 
   private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) throws OrmException {
     try {
-      return Optional.of(notesFactory.createChecked(db, project.getNameKey(), changeId));
+      return Optional.of(notesFactory.createChecked(project.getNameKey(), changeId));
     } catch (NoSuchChangeException e) {
       return Optional.empty();
     }
@@ -3246,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/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 7bb16d81..6636b1f 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;
@@ -116,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;
@@ -162,7 +160,6 @@
   @Inject
   ReplaceOp(
       AccountResolver accountResolver,
-      ApprovalCopier approvalCopier,
       ApprovalsUtil approvalsUtil,
       ChangeData.Factory changeDataFactory,
       ChangeKindCache changeKindCache,
@@ -190,7 +187,6 @@
       @Assisted @Nullable MagicBranchInput magicBranch,
       @Assisted @Nullable PushCertificate pushCertificate) {
     this.accountResolver = accountResolver;
-    this.approvalCopier = approvalCopier;
     this.approvalsUtil = approvalsUtil;
     this.changeDataFactory = changeDataFactory;
     this.changeKindCache = changeKindCache;
@@ -250,16 +246,16 @@
           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);
@@ -305,7 +301,6 @@
 
     newPatchSet =
         psUtil.insert(
-            ctx.getDb(),
             ctx.getRevWalk(),
             update,
             patchSetId,
@@ -316,25 +311,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),
@@ -353,7 +334,7 @@
     }
 
     msg = createChangeMessage(ctx, reviewMessage);
-    cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+    cmUtil.addChangeMessage(update, msg);
 
     if (mergedByPushOp == null) {
       resetChange(ctx);
@@ -466,7 +447,6 @@
     if (!approvals.isEmpty()) {
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
-              ctx.getDb(),
               ctx.getNotes(),
               priorPatchSetId,
               ctx.getAccountId(),
@@ -508,7 +488,7 @@
   private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress)
       throws OrmException {
     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;
@@ -528,13 +508,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(),
@@ -571,10 +549,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 b228267..1897d62 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -33,7 +33,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 +98,7 @@
     private final AccountValidator accountValidator;
     private final String installCommitMsgHookCommand;
     private final ProjectCache projectCache;
+    private final ProjectConfig.Factory projectConfigFactory;
 
     @Inject
     Factory(
@@ -111,7 +111,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 +124,7 @@
       this.installCommitMsgHookCommand =
           cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
       this.projectCache = projectCache;
+      this.projectConfigFactory = projectConfigFactory;
     }
 
     public CommitValidators forReceiveCommits(
@@ -151,7 +153,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),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -161,7 +163,7 @@
 
     public CommitValidators forGerritCommits(
         PermissionBackend.ForProject forProject,
-        NameKey branch,
+        Branch.NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         RevWalk rw,
@@ -183,7 +185,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),
@@ -342,7 +344,8 @@
             .append(getCommitMessageHookInstallationHint())
             .append("\n")
             .append("and then amend the commit:\n")
-            .append("  git commit --amend --no-edit\n");
+            .append("  git commit --amend --no-edit\n")
+            .append("Finally, push your changes again\n");
       }
       return new CommitValidationMessage(sb.toString(), Type.ERROR);
     }
@@ -389,6 +392,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;
@@ -396,11 +400,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;
@@ -415,7 +421,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 0422c51..1dd48f1 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -26,7 +26,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;
@@ -46,7 +45,6 @@
 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,7 +296,7 @@
 
       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;
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index acae533..dd5d508 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..abc8c90 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -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
@@ -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.
    */
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/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 82a1bf4..77af248 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -21,6 +21,8 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.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;
@@ -34,18 +36,16 @@
 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.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;
@@ -75,13 +75,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 final GitRepositoryManager repoManager;
@@ -91,14 +102,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,
@@ -114,7 +159,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;
@@ -135,7 +214,7 @@
 
   private static MetaDataUpdateFactory getMetaDataUpdateFactory(
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
-      @Nullable IdentifiedUser currentUser,
+      Optional<IdentifiedUser> currentUser,
       PersonIdent serverIdent,
       AuditLogFormatter auditLogFormatter) {
     return (projectName, repository, batchRefUpdate) -> {
@@ -143,10 +222,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;
       }
@@ -156,8 +235,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) {
@@ -342,7 +421,7 @@
 
     RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
     gitRefUpdated.fire(
-        allUsersName, batchRefUpdate, currentUser != null ? currentUser.state() : null);
+        allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
   }
 
   private void updateCachesOnGroupCreation(InternalGroup createdGroup) throws IOException {
@@ -390,20 +469,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());
@@ -411,25 +490,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/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index 3c9538c..3d5c1a7 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -76,7 +76,7 @@
   }
 
   public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
-      ImmutableList.<SchemaDefinitions<?>>of(
+      ImmutableList.of(
           AccountSchemaDefinitions.INSTANCE,
           ChangeSchemaDefinitions.INSTANCE,
           GroupSchemaDefinitions.INSTANCE,
@@ -125,7 +125,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 +160,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..e7bdfea 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -39,16 +39,13 @@
       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);
-          }
+      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);
         }
       };
 
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index ec0e1d4..8a1776d 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -18,11 +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 com.google.gerrit.server.plugincontext.PluginSetContext;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -35,7 +35,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 +43,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();
@@ -68,9 +68,7 @@
               } finally {
                 running.set(false);
                 if (!ok) {
-                  for (OnlineUpgradeListener listener : listeners) {
-                    listener.onFailure(name, oldVersion, newVersion);
-                  }
+                  listeners.runEach(listener -> listener.onFailure(name, oldVersion, newVersion));
                 }
               }
             }
@@ -94,9 +92,7 @@
   }
 
   private void reindex() throws IOException {
-    for (OnlineUpgradeListener listener : listeners) {
-      listener.onStart(name, oldVersion, newVersion);
-    }
+    listeners.runEach(listener -> listener.onStart(name, oldVersion, newVersion));
     index =
         requireNonNull(
             indexes.getWriteIndex(newVersion),
@@ -118,9 +114,7 @@
     }
     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() {
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/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index e1b0edd..f1b8591 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -121,6 +121,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..18f44f0 100644
--- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
+++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -17,9 +17,9 @@
 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;
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index b0f4de76..577c255 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;
@@ -38,7 +37,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;
@@ -58,7 +56,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;
@@ -68,14 +65,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;
@@ -215,27 +210,26 @@
 
     @Override
     public Void call() throws Exception {
-      try (Repository repo = repoManager.openRepository(project);
-          ReviewDb db = schemaFactory.open()) {
+      try (Repository repo = repoManager.openRepository(project)) {
         // Order of scanning changes is undefined. This is ok if we assume that packfile locality is
         // not important for indexing, since sites should have a fully populated DiffSummary cache.
         // 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());
       }
       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..50b38d3 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,10 +72,7 @@
 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;
@@ -186,6 +187,102 @@
   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) throws OrmException {
+    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) throws OrmException {
+    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) throws OrmException {
+    try {
+      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));
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /** 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) throws OrmException {
+    try {
+      return cd.commitFooters().stream()
+          .map(f -> f.toString().toLowerCase(Locale.US))
+          .collect(toSet());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /** 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) throws OrmException {
+    List<String> paths;
+    try {
+      paths = cd.currentFilePaths();
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+
+    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 +395,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 +448,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",
@@ -469,12 +566,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 +607,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 +695,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 =
@@ -798,19 +902,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,11 +937,9 @@
                 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;
               });
 
@@ -851,22 +951,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 064af64..f1120b2 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
 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;
@@ -26,28 +25,18 @@
 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;
@@ -55,7 +44,6 @@
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 
@@ -86,9 +74,6 @@
 
   @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;
@@ -100,9 +85,6 @@
   @AssistedInject
   ChangeIndexer(
       @GerritServerConfig Config cfg,
-      SchemaFactory<ReviewDb> schemaFactory,
-      NotesMigration notesMigration,
-      ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
@@ -111,9 +93,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;
@@ -126,10 +105,7 @@
 
   @AssistedInject
   ChangeIndexer(
-      SchemaFactory<ReviewDb> schemaFactory,
       @GerritServerConfig Config cfg,
-      NotesMigration notesMigration,
-      ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       PluginSetContext<ChangeIndexedListener> indexedListeners,
@@ -138,9 +114,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;
@@ -236,23 +209,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) throws IOException {
+    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) throws IOException {
+    index(changeDataFactory.create(project, changeId));
   }
 
   /**
@@ -329,7 +299,7 @@
       this.id = id;
     }
 
-    protected abstract T callImpl(Provider<ReviewDb> db) throws Exception;
+    protected abstract T callImpl() throws Exception;
 
     @Override
     public abstract String toString();
@@ -337,37 +307,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);
@@ -382,8 +330,8 @@
     }
 
     @Override
-    public Void callImpl(Provider<ReviewDb> db) throws Exception {
-      ChangeData cd = newChangeData(db.get(), project, id);
+    public Void callImpl() throws Exception {
+      ChangeData cd = changeDataFactory.create(project, id);
       index(cd);
       return null;
     }
@@ -394,7 +342,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;
 
@@ -426,14 +374,12 @@
     }
 
     @Override
-    public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
+    public Boolean callImpl() throws Exception {
       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;
@@ -460,26 +406,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/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 66f8df2..8088837 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -19,25 +19,25 @@
 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 +52,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(
@@ -98,8 +98,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);
         }
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index 609432b..669bb1e 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -27,7 +27,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;
@@ -182,13 +181,10 @@
     @Override
     protected Void impl(RequestContext ctx) throws OrmException, IOException {
       // Reload change, as some time may have passed since GetChanges.
-      ReviewDb db = ctx.getReviewDbProvider().get();
       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..4ec29c7 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) throws IOException {
     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/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/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index 15e67af..ea91929 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -3,7 +3,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..3a5a2cf 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -18,7 +18,7 @@
 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;
@@ -29,6 +29,7 @@
 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 +37,7 @@
 
   public static MailRecipients getRecipientsFromFooters(
       AccountResolver accountResolver, List<FooterLine> footerLines)
-      throws OrmException, IOException {
+      throws OrmException, IOException, ConfigInvalidException {
     MailRecipients recipients = new MailRecipients();
     for (FooterLine footerLine : footerLines) {
       try {
@@ -45,7 +46,7 @@
         } else if (footerLine.matches(FooterKey.CC)) {
           recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
         }
-      } catch (NoSuchAccountException e) {
+      } catch (UnprocessableEntityException e) {
         continue;
       }
     }
@@ -59,13 +60,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 OrmException, UnprocessableEntityException, IOException, ConfigInvalidException {
+    return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().getAccount().getId();
   }
 
   private static boolean isReviewer(FooterLine candidateFooterLine) {
@@ -124,7 +122,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..39cd9c3 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -17,10 +17,8 @@
 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.extensions.client.Side;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -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();
     }
@@ -286,14 +284,14 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
-      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      patchSet = psUtil.get(ctx.getNotes(), psId);
       notes = ctx.getNotes();
       if (patchSet == null) {
         throw new OrmException("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.
@@ -372,7 +361,6 @@
         ChangeContext ctx, MailComment mailComment, PatchSet current) throws OrmException {
       if (mailComment.getInReplyTo() != null) {
         return psUtil.get(
-            ctx.getDb(),
             ctx.getNotes(),
             new PatchSet.Id(ctx.getChange().getId(), mailComment.getInReplyTo().key.patchSetId));
       }
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 8b8f6f9..012d212 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -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;
@@ -159,8 +159,7 @@
       setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.getPatchSetId() + "");
       if (patchSetInfo == null) {
         try {
-          patchSetInfo =
-              args.patchSetInfoFactory.get(args.db.get(), changeData.notes(), patchSet.getId());
+          patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.getId());
         } catch (PatchSetInfoNotAvailableException | OrmException err) {
           patchSetInfo = null;
         }
@@ -184,7 +183,7 @@
     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));
@@ -294,9 +293,7 @@
       ps = patchSet;
     } else {
       try {
-        ps =
-            args.patchSetUtil.get(
-                changeData.db(), changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
+        ps = args.patchSetUtil.get(changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
       } catch (OrmException e) {
         throw new PatchListNotAvailableException("Failed to get patchSet");
       }
@@ -326,7 +323,7 @@
 
   /** BCC any user who has starred this change. */
   protected void bccStarredBy() {
-    if (!NotifyHandling.ALL.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())) {
       return;
     }
 
@@ -348,7 +345,7 @@
   @Override
   protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
       throws OrmException {
-    if (!NotifyHandling.ALL.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())) {
       return new Watchers();
     }
 
@@ -358,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;
     }
 
@@ -373,7 +371,8 @@
 
   /** 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;
     }
 
@@ -399,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;
@@ -414,7 +409,7 @@
   protected Set<Account.Id> getAuthors() {
     Set<Account.Id> authors = new HashSet<>();
 
-    switch (notify) {
+    switch (notify.handling()) {
       case NONE:
         break;
       case ALL:
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 69a7926..0f55366 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -153,10 +153,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());
     }
@@ -315,7 +315,7 @@
 
     Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, child.key.patchSetId);
     try {
-      return commentsUtil.getPublished(args.db.get(), changeData.notes(), key);
+      return commentsUtil.getPublished(changeData.notes(), key);
     } catch (OrmException 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/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/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index 34f959c..b524c83 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -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;
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index f94f1ca..c45da40 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -59,7 +59,7 @@
 
     setHeader("Message-ID", getChangeMessageThreadId());
 
-    switch (notify) {
+    switch (notify.handling()) {
       case NONE:
       case OWNER:
         break;
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 1ef3b25..a6747b5 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -18,11 +18,8 @@
 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.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
@@ -33,6 +30,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;
@@ -65,13 +63,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;
@@ -83,21 +80,17 @@
     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.
    *
    * @throws EmailException
    */
   public void send() throws EmailException {
-    if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) {
+    if (!notify.shouldNotify()) {
       return;
     }
 
@@ -129,7 +122,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.
             //
@@ -238,8 +231,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);
@@ -412,7 +405,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/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 2398b82..f2844c4 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -63,7 +63,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/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 72f1ba6..cecbbe8 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.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() {
@@ -146,19 +136,7 @@
       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()) {
+    if (args.failOnLoadForTest.get()) {
       throw new OrmException("Reading from NoteDb is disabled");
     }
     try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
@@ -166,12 +144,8 @@
         // 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);
@@ -199,10 +173,10 @@
   }
 
   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() throws OrmException {
     loaded = false;
     return load();
   }
@@ -210,8 +184,6 @@
   public ObjectId loadRevision() throws OrmException {
     if (loaded) {
       return getRevision();
-    } else if (!args.migration.readChanges()) {
-      return null;
     }
     try (Repository repo = args.repoManager.openRepository(getProjectName())) {
       Ref ref = repo.getRefDatabase().exactRef(getRefName());
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index e0cc771..daac83f 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -29,10 +29,8 @@
 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 +42,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 +57,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 +71,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 +85,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 +93,6 @@
     this.realAccountId = realAccountId;
     this.authorIdent = authorIdent;
     this.when = when;
-    this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg);
   }
 
   private static void checkUserType(CurrentUser user) {
@@ -215,12 +203,7 @@
       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 +236,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.
    *
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 169fd2e..5a3ebd6 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -28,7 +28,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;
@@ -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;
   }
 
@@ -211,19 +185,16 @@
 
   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;
-          }
+    // 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;
         }
       }
     }
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..af85661 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,15 @@
 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.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -51,15 +47,10 @@
 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;
@@ -69,15 +60,12 @@
 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,24 +103,14 @@
       this.projectCache = projectCache;
     }
 
-    public ChangeNotes createChecked(ReviewDb db, Change c) throws OrmException {
-      return createChecked(db, c.getProject(), c.getId());
+    public ChangeNotes createChecked(Change c) throws OrmException {
+      return createChecked(c.getProject(), c.getId());
     }
 
-    public ChangeNotes createChecked(ReviewDb db, Project.NameKey project, Change.Id changeId)
+    public ChangeNotes createChecked(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();
+      Change change = newChange(project, changeId);
+      return new ChangeNotes(args, change, true, null).load();
     }
 
     public ChangeNotes createChecked(Change.Id changeId) throws OrmException {
@@ -153,43 +126,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) throws OrmException {
       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,188 +144,89 @@
      * @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();
+      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) throws OrmException {
+      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) throws OrmException {
       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)
+        Project.NameKey project, Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate)
         throws OrmException {
       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) {
@@ -390,7 +235,7 @@
       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) {
@@ -458,7 +303,6 @@
   // notes easier.
   RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
-  private NoteDbUpdateManager.Result rebuildResult;
   private DraftCommentNotes draftCommentNotes;
   private RobotCommentNotes robotCommentNotes;
 
@@ -469,13 +313,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;
@@ -608,8 +447,7 @@
    */
   private void loadDraftComments(Account.Id author, @Nullable Ref ref) throws OrmException {
     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();
     }
   }
@@ -658,19 +496,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 +508,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 +528,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 cbb7020..7ba5679 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);
@@ -933,20 +919,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) {
@@ -1028,7 +1000,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 0d9b962..10f1f04 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;
@@ -62,7 +61,6 @@
 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;
@@ -71,7 +69,6 @@
 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 +81,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;
@@ -152,7 +148,6 @@
   private boolean isAllowWriteToNewtRef;
   private String psDescription;
   private boolean currentPatchSet;
-  private Timestamp readOnlyUntil;
   private Boolean isPrivate;
   private Boolean workInProgress;
   private Integer revertOf;
@@ -164,9 +159,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -176,9 +169,7 @@
       @Assisted CurrentUser user,
       ChangeNoteUtil noteUtil) {
     this(
-        cfg,
         serverIdent,
-        migration,
         updateManagerFactory,
         draftUpdateFactory,
         robotCommentUpdateFactory,
@@ -192,9 +183,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -205,9 +194,7 @@
       @Assisted Date when,
       ChangeNoteUtil noteUtil) {
     this(
-        cfg,
         serverIdent,
-        migration,
         updateManagerFactory,
         draftUpdateFactory,
         robotCommentUpdateFactory,
@@ -226,9 +213,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -238,7 +223,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;
@@ -248,9 +233,7 @@
 
   @AssistedInject
   private ChangeUpdate(
-      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
-      NotesMigration migration,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
@@ -262,17 +245,7 @@
       @Assisted PersonIdent authorIdent,
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator) {
-    super(
-        cfg,
-        migration,
-        noteUtil,
-        serverIdent,
-        null,
-        change,
-        accountId,
-        realAccountId,
-        authorIdent,
-        when);
+    super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
     this.draftUpdateFactory = draftUpdateFactory;
     this.robotCommentUpdateFactory = robotCommentUpdateFactory;
     this.updateManagerFactory = updateManagerFactory;
@@ -283,7 +256,6 @@
   public ObjectId commit() throws IOException, OrmException {
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
       updateManager.add(this);
-      updateManager.stageAndApplyDelta(getChange());
       updateManager.execute();
     }
     return getResult();
@@ -545,16 +517,13 @@
     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));
@@ -575,7 +544,7 @@
     // 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
@@ -750,10 +719,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);
     }
@@ -813,7 +778,6 @@
         && tag == null
         && psDescription == null
         && !currentPatchSet
-        && readOnlyUntil == null
         && isPrivate == null
         && workInProgress == null
         && revertOf == null;
@@ -852,10 +816,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 +837,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
index 361c435..1af8494 100644
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
@@ -19,6 +19,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.Project;
@@ -26,7 +28,6 @@
 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;
@@ -50,6 +51,7 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.MutableInteger;
 
+@UsedAt(UsedAt.Project.GOOGLE)
 @Singleton
 public class CommentJsonMigrator {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -96,7 +98,7 @@
       }
 
       progress.refsUpdated =
-          bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
+          bru.getCommands().stream().map(ReceiveCommand::getRefName).collect(toImmutableList());
       if (!bru.getCommands().isEmpty()) {
         if (!dryRun) {
           ins.flush();
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 656e9ec..4f1a5d0 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -142,7 +142,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..f8c713c
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -0,0 +1,129 @@
+// 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.git.RefUpdateUtil;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gwtorm.server.OrmException;
+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, OrmException {
+    try (ObjectReader or = repo.newObjectReader()) {
+      return parse(repo, refName, or);
+    }
+  }
+
+  public static Optional<IntBlob> parse(Repository repo, String refName, RevWalk rw)
+      throws IOException, OrmException {
+    return parse(repo, refName, rw.getObjectReader());
+  }
+
+  private static Optional<IntBlob> parse(Repository repo, String refName, ObjectReader or)
+      throws IOException, OrmException {
+    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 OrmException("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/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 12448fb..7f23f87 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -17,21 +17,17 @@
 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.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 +36,14 @@
 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,37 @@
   }
 
   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 OrmException, 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();
     }
@@ -614,11 +382,9 @@
 
   @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);
-    }
+    checkNotExecuted();
     if (isEmpty()) {
+      executed = true;
       return null;
     }
     try (Timer1.Context timer = metrics.updateLatency.start(CHANGES)) {
@@ -633,6 +399,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 +427,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);
 
@@ -731,7 +498,6 @@
     for (Change.Id id : toDelete) {
       doDelete(id);
     }
-    checkExpectedState();
   }
 
   private void doDelete(Change.Id id) throws IOException {
@@ -751,74 +517,8 @@
     }
   }
 
-  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(
@@ -887,8 +587,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..56264e9 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -27,13 +27,10 @@
 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.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -42,18 +39,15 @@
 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;
@@ -75,9 +69,9 @@
   }
 
   @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");
@@ -234,7 +228,7 @@
     try {
       try (Repository repo = repoManager.openRepository(projectName);
           RevWalk rw = new RevWalk(repo)) {
-        checkResult(store(repo, rw, null, val));
+        IntBlob.store(repo, rw, projectName, refName, null, val, gitRefUpdated);
         counter = limit;
       } catch (IOException e) {
         throw new OrmException(e);
@@ -250,7 +244,11 @@
       try (Repository repo = repoManager.openRepository(projectName);
           RevWalk rw = new RevWalk(repo)) {
         TryIncreaseTo attempt = new TryIncreaseTo(repo, rw, val);
-        checkResult(retryer.call(attempt));
+        RefUpdate ru = retryer.call(attempt);
+        // Null update is a sentinel meaning nothing to do.
+        if (ru != null) {
+          RefUpdateUtil.checkResult(ru);
+        }
         counter = limit;
       } catch (ExecutionException | RetryException e) {
         if (e.getCause() != null) {
@@ -269,7 +267,7 @@
     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++;
@@ -283,17 +281,7 @@
     }
   }
 
-  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,23 +295,23 @@
     }
 
     @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 class TryIncreaseTo implements Callable<RefUpdate> {
     private final Repository repo;
     private final RevWalk rw;
     private final int value;
@@ -335,60 +323,26 @@
     }
 
     @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();
       } else {
-        oldId = ref.getObjectId();
-        int next = parse(rw, oldId);
+        oldId = blob.get().id();
+        int next = blob.get().value();
         if (next >= value) {
-          // a concurrent write updated the ref already to this or a higher value
-          return RefUpdate.Result.NO_CHANGE;
+          // A concurrent write updated the ref already to this or a higher value; return null as a
+          // sentinel meaning nothing to do. Returning RefUpdate doesn't give us the flexibility to
+          // return any other kind of sentinel, since it's a fairly thick object.
+          return null;
         }
       }
-      return store(repo, rw, oldId, value);
+      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, value, gitRefUpdated);
     }
   }
 
-  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..9307724 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -25,7 +25,6 @@
 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;
@@ -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) {
@@ -178,19 +152,16 @@
     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;
         }
       }
     }
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..1a255f14
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -0,0 +1,126 @@
+// 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.gwtorm.server.OrmException;
+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() throws OrmException {
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
+      return accountSeq.next();
+    }
+  }
+
+  public int nextChangeId() throws OrmException {
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
+      return changeSeq.next();
+    }
+  }
+
+  public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
+      return changeSeq.next(count);
+    }
+  }
+
+  public int nextGroupId() throws OrmException {
+    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 1ac1da2..0000000
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ /dev/null
@@ -1,1053 +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;
-import org.eclipse.jgit.util.io.NullOutputStream;
-
-/** 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);
-  }
-
-  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 = NullOutputStream.INSTANCE;
-    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/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index b1e0e3c..50d3711 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;
@@ -82,7 +81,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 +110,6 @@
       PatchSetUtil psUtil,
       Provider<PatchScriptBuilder> builderFactory,
       PatchListCache patchListCache,
-      ReviewDb db,
       CommentsUtil commentsUtil,
       ChangeEditUtil editReader,
       Provider<CurrentUser> userProvider,
@@ -127,7 +124,6 @@
     this.psUtil = psUtil;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
-    this.db = db;
     this.notes = notes;
     this.commentsUtil = commentsUtil;
     this.editReader = editReader;
@@ -150,7 +146,6 @@
       PatchSetUtil psUtil,
       Provider<PatchScriptBuilder> builderFactory,
       PatchListCache patchListCache,
-      ReviewDb db,
       CommentsUtil commentsUtil,
       ChangeEditUtil editReader,
       Provider<CurrentUser> userProvider,
@@ -165,7 +160,6 @@
     this.psUtil = psUtil;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
-    this.db = db;
     this.notes = notes;
     this.commentsUtil = commentsUtil;
     this.editReader = editReader;
@@ -200,11 +194,11 @@
     }
     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);
       }
@@ -308,7 +302,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) {
@@ -391,7 +385,7 @@
   }
 
   private void loadPublished(Map<Patch.Key, Patch> byKey, String file) throws OrmException {
-    for (Comment c : commentsUtil.publishedByChangeFile(db, notes, changeId, 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);
@@ -404,7 +398,7 @@
 
   private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file)
       throws OrmException {
-    for (Comment c : commentsUtil.draftByChangeFileAuthor(db, notes, file, me)) {
+    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..f11f116 100644
--- a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -20,7 +20,6 @@
 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;
@@ -66,10 +65,10 @@
     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) {
       throw new PatchSetInfoNotAvailableException(e);
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index f4e659e..309bf51 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;
 
@@ -28,14 +27,12 @@
 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,9 @@
       this.notesFactory = notesFactory;
     }
 
-    ChangeControl create(
-        RefControl refControl, ReviewDb db, Project.NameKey project, Change.Id changeId)
+    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId)
         throws OrmException {
-      return create(refControl, notesFactory.create(db, project, changeId));
+      return create(refControl, notesFactory.create(project, changeId));
     }
 
     ChangeControl create(RefControl refControl, ChangeNotes notes) {
@@ -77,8 +73,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 +90,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) throws OrmException {
+    if (getChange().isPrivate() && !isPrivateVisible(cd)) {
       return false;
     }
     return refControl.isVisible();
@@ -158,9 +154,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) throws OrmException {
     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 +165,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 +176,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 +212,9 @@
         || getProjectControl().isAdmin();
   }
 
-  private boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
+  private boolean isPrivateVisible(ChangeData cd) throws OrmException {
     return isOwner()
-        || isReviewer(db, cd)
+        || isReviewer(cd)
         || refControl.canPerform(Permission.VIEW_PRIVATE_CHANGES)
         || getUser().isInternalUser();
   }
@@ -220,26 +224,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 +286,7 @@
       try {
         switch (perm) {
           case READ:
-            return isVisible(db(), changeData());
+            return isVisible(changeData());
           case ABANDON:
             return canAbandon();
           case DELETE:
@@ -316,6 +307,8 @@
             return canRestore();
           case SUBMIT:
             return refControl.canSubmit(isOwner());
+          case TOGGLE_WORK_IN_PROGRESS_STATE:
+            return canToggleWorkInProgressState();
 
           case REMOVE_REVIEWER:
           case SUBMIT_AS:
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..913921f 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -14,15 +14,20 @@
 
 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;
@@ -36,7 +41,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;
@@ -52,7 +56,6 @@
 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;
@@ -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 (OrmException 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..3ac294a 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -15,12 +15,15 @@
 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.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 +31,16 @@
 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;
@@ -150,35 +151,14 @@
     // 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. */
@@ -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();
 
@@ -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 787bee4..144519a 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -17,9 +17,12 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
 
+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.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;
@@ -27,7 +30,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;
@@ -46,7 +48,6 @@
 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;
@@ -98,9 +99,9 @@
     return new ForProjectImpl();
   }
 
-  ChangeControl controlFor(ReviewDb db, Change change) throws OrmException {
+  ChangeControl controlFor(Change change) throws OrmException {
     return changeControlFactory.create(
-        controlForRef(change.getDest()), db, change.getProject(), change.getId());
+        controlForRef(change.getDest()), change.getProject(), change.getId());
   }
 
   ChangeControl controlFor(ChangeNotes notes) {
@@ -344,7 +345,7 @@
 
     @Override
     public ForRef ref(String ref) {
-      return controlForRef(ref).asForRef().database(db);
+      return controlForRef(ref).asForRef();
     }
 
     @Override
@@ -373,17 +374,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);
         }
@@ -392,7 +394,7 @@
     }
 
     @Override
-    public BooleanCondition testCond(ProjectPermission perm) {
+    public BooleanCondition testCond(CoreOrPluginProjectPermission perm) {
       return new PermissionBackendCondition.ForProject(this, perm, getUser());
     }
 
@@ -405,6 +407,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..60fd15b 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -34,7 +34,6 @@
 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,9 +441,7 @@
     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()));
+        return getProjectControl().controlFor(cd.change()).asForChange(cd);
       } catch (OrmException 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/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/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 81cc78b..6dd697f 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;
@@ -52,13 +54,17 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.config.AllProjectsName;
 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;
@@ -71,7 +77,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;
@@ -82,8 +87,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";
@@ -127,6 +135,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";
@@ -165,6 +175,51 @@
 
   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;
+    }
+  }
+
+  private final StoredConfig baseConfig;
+
   private Project project;
   private AccountsSection accountsSection;
   private GroupList groupList;
@@ -175,7 +230,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;
@@ -186,20 +241,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;
-  }
-
   public static CommentLinkInfoImpl buildCommentLink(Config cfg, String name, boolean allowRaw)
       throws IllegalArgumentException {
     String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
@@ -238,11 +279,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 {
@@ -319,7 +361,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);
       }
@@ -415,7 +457,7 @@
   }
 
   public Collection<CommentLinkInfoImpl> getCommentLinkSections() {
-    return commentLinkSections;
+    return commentLinkSections.values();
   }
 
   public ConfiguredMimeTypes getMimeTypes() {
@@ -504,11 +546,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;
@@ -593,6 +638,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(
@@ -700,7 +748,7 @@
         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);
             }
           }
@@ -708,7 +756,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,
@@ -737,6 +785,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);
@@ -753,6 +807,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,
@@ -938,10 +1008,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(
@@ -1128,7 +1198,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);
@@ -1163,13 +1233,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 =
@@ -1204,6 +1284,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<>();
@@ -1281,7 +1365,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);
         }
@@ -1402,7 +1486,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)) {
@@ -1464,6 +1548,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..b50b046
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -0,0 +1,255 @@
+// 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.");
+    } catch (RepositoryNotFoundException badName) {
+      throw new BadRequestException("invalid project name: " + nameKey);
+    } 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 75ce483..8d6923b 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;
@@ -32,7 +30,6 @@
 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 +46,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 +53,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;
@@ -85,7 +78,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;
@@ -106,11 +98,6 @@
   /** 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;
 
@@ -119,7 +106,6 @@
 
   @Inject
   public ProjectState(
-      SitePaths sitePaths,
       ProjectCache projectCache,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
@@ -129,7 +115,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 +406,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);
   }
 
   /**
@@ -553,24 +533,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());
   }
@@ -602,26 +564,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()) {
@@ -630,10 +572,6 @@
     return project;
   }
 
-  private String readFile(Path p) throws IOException {
-    return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
-  }
-
   private LabelTypes loadLabelTypes() {
     Map<String, LabelType> types = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
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/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index df98f5e..7353ce8 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;
@@ -29,18 +28,15 @@
 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;
   }
 
   /**
@@ -75,11 +71,7 @@
         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 +82,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 +92,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..3095f2f 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;
 
 /**
@@ -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 7150fae..1afa426 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -102,7 +102,7 @@
       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);
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/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 57a0dcc..732177d 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -126,7 +126,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>
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 148c633..b7e8a46 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
@@ -27,7 +26,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;
@@ -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;
     }
@@ -126,11 +121,7 @@
     }
 
     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) {
       throw error(String.format("change %s not found", change));
     }
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index fb3549c..1cb2170 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -16,24 +16,19 @@
 
 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;
   }
@@ -43,7 +38,6 @@
     try {
       permissionBackend
           .absentUser(accountState.getAccount().getId())
-          .database(db)
           .change(changeNotes)
           .check(ChangePermission.READ);
       return true;
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index e8e72a8..61a9690 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -42,7 +42,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> {
   @Inject
   InternalAccountQuery(
       AccountQueryProcessor queryProcessor,
@@ -51,31 +51,6 @@
     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 {
     return query(AccountPredicates.defaultPredicate(schema(), true, query));
   }
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 83d68db..b058166 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,6 +29,7 @@
 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;
@@ -46,7 +46,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 +63,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;
@@ -76,7 +74,6 @@
 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;
@@ -104,8 +101,6 @@
 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 {
     List<Change> result = new ArrayList<>(changeDatas.size());
     for (ChangeData cd : changeDatas) {
@@ -122,24 +117,10 @@
     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();
     }
   }
 
@@ -147,29 +128,10 @@
     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();
-        }
-      }
+    for (ChangeData cd : changes) {
+      cd.patchSets();
     }
   }
 
@@ -177,24 +139,10 @@
     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.currentPatchSet();
     }
   }
 
@@ -203,30 +151,10 @@
     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.currentApprovals();
     }
   }
 
@@ -234,30 +162,10 @@
     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.messages();
     }
   }
 
@@ -265,7 +173,7 @@
       throws OrmException {
     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 +195,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 +231,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 +246,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 +254,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 +296,7 @@
   private PersonIdent committer;
   private int parentCount;
   private Integer unresolvedCommentCount;
+  private Integer totalCommentCount;
   private LabelTypes labelTypes;
 
   private ImmutableList<byte[]> refStates;
@@ -407,14 +313,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 +331,6 @@
     this.repoManager = repoManager;
     this.mergeUtilFactory = mergeUtilFactory;
     this.mergeabilityCache = mergeabilityCache;
-    this.notesMigration = notesMigration;
     this.patchListCache = patchListCache;
     this.psUtil = psUtil;
     this.projectCache = projectCache;
@@ -436,11 +339,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,10 +358,6 @@
     return this;
   }
 
-  public ReviewDb db() {
-    return db;
-  }
-
   public AllUsersName getAllUsersNameForIndexing() {
     return allUsersName;
   }
@@ -569,7 +463,7 @@
 
   public Change reloadChange() throws OrmException {
     try {
-      notes = notesFactory.createChecked(db, project, legacyId);
+      notes = notesFactory.createChecked(project, legacyId);
     } catch (NoSuchChangeException e) {
       throw new OrmException("Unable to load change " + legacyId, e);
     }
@@ -596,7 +490,7 @@
       if (!lazyLoad) {
         throw new OrmException("ChangeNotes not available, lazyLoad = false");
       }
-      notes = notesFactory.create(db, project(), legacyId);
+      notes = notesFactory.create(project(), legacyId);
     }
     return notes;
   }
@@ -629,7 +523,7 @@
         try {
           currentApprovals =
               ImmutableList.copyOf(
-                  approvalsUtil.byPatchSet(db, notes(), c.currentPatchSetId(), null, null));
+                  approvalsUtil.byPatchSet(notes(), c.currentPatchSetId(), null, null));
         } catch (OrmException e) {
           if (e.getCause() instanceof NoSuchChangeException) {
             currentApprovals = Collections.emptyList();
@@ -712,7 +606,7 @@
    */
   public Collection<PatchSet> patchSets() throws OrmException {
     if (patchSets == null) {
-      patchSets = psUtil.byChange(db, notes());
+      patchSets = psUtil.byChange(notes());
     }
     return patchSets;
   }
@@ -748,7 +642,7 @@
       if (!lazyLoad) {
         return ImmutableListMultimap.of();
       }
-      allApprovals = approvalsUtil.byChange(db, notes());
+      allApprovals = approvalsUtil.byChange(notes());
     }
     return allApprovals;
   }
@@ -856,7 +750,7 @@
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      publishedComments = commentsUtil.publishedByChange(db, notes());
+      publishedComments = commentsUtil.publishedByChange(notes());
     }
     return publishedComments;
   }
@@ -925,12 +819,29 @@
     this.unresolvedCommentCount = count;
   }
 
+  public Integer totalCommentCount() throws OrmException {
+    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() throws OrmException {
     if (messages == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      messages = cmUtil.byChange(db, notes());
+      messages = cmUtil.byChange(notes());
     }
     return messages;
   }
@@ -975,9 +886,9 @@
       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;
@@ -1063,23 +974,17 @@
       }
 
       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);
         }
       }
     }
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..a793cf2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -18,7 +18,6 @@
 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;
@@ -29,6 +28,7 @@
 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;
@@ -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) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index fb34393..ab0c947 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;
@@ -46,7 +48,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;
@@ -54,6 +55,7 @@
 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.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupMembers;
@@ -68,7 +70,6 @@
 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;
@@ -123,8 +124,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";
@@ -138,7 +139,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";
@@ -208,12 +213,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;
@@ -224,7 +227,6 @@
     @Inject
     @VisibleForTesting
     public Arguments(
-        Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
         ChangeIndexRewriter rewriter,
         DynamicMap<ChangeOperatorFactory> opFactories,
@@ -249,11 +251,9 @@
         IndexConfig indexConfig,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
-        NotesMigration notesMigration,
         GroupMembers groupMembers,
         Provider<AnonymousUser> anonymousUserProvider) {
       this(
-          db,
           queryProvider,
           rewriter,
           opFactories,
@@ -278,13 +278,11 @@
           indexConfig,
           starredChangesUtil,
           accountCache,
-          notesMigration,
           groupMembers,
           anonymousUserProvider);
     }
 
     private Arguments(
-        Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
         ChangeIndexRewriter rewriter,
         DynamicMap<ChangeOperatorFactory> opFactories,
@@ -309,10 +307,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;
@@ -337,14 +333,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,
@@ -369,7 +363,6 @@
           indexConfig,
           starredChangesUtil,
           accountCache,
-          notesMigration,
           groupMembers,
           anonymousUserProvider);
     }
@@ -565,9 +558,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)) {
@@ -747,6 +740,58 @@
   }
 
   @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)) {
+      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);
+      }
+
+      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 {
     Set<Account.Id> accounts = null;
@@ -904,26 +949,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 {
     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<>();
@@ -938,7 +981,6 @@
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
     return new ChangeIsVisibleToPredicate(
-        args.db,
         args.notesFactory,
         user,
         args.permissionBackend,
@@ -1298,14 +1340,14 @@
 
   private Set<Account.Id> parseAccount(String who)
       throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    if (isSelf(who)) {
-      return Collections.singleton(self());
+    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");
-    }
-    return matches;
   }
 
   private GroupReference parseGroup(String group) throws QueryParseException {
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..5a7efb8 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -41,7 +41,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;
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..ada9f27 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -14,6 +14,11 @@
 
 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.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -21,6 +26,7 @@
 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;
@@ -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 (IOException | OrmException 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 | OrmException | 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;
       }
     }
 
@@ -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/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
new file mode 100644
index 0000000..676a208
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.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.query.change;
+
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+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) throws OrmException {
+    return ChangeField.getDirectories(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 54e22f3..430d8c3 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;
@@ -30,14 +29,12 @@
 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 +46,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;
@@ -123,8 +119,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/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
new file mode 100644
index 0000000..fc33b65
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.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 static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+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) throws OrmException {
+    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..5353f11
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+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) throws OrmException {
+    return ChangeField.getExtensions(object).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
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..1d7d19b
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -0,0 +1,46 @@
+// 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 com.google.gwtorm.server.OrmException;
+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) throws OrmException {
+    return ChangeField.getFooters(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 495d27c..6bd9025 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,7 +32,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.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
@@ -46,6 +44,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 +55,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,31 +91,6 @@
     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 {
     return byKeyPrefix(key.get());
   }
@@ -168,11 +142,10 @@
   }
 
   public Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo, ReviewDb db, Branch.NameKey branch, Collection<String> hashes)
+      Repository repo, Branch.NameKey branch, Collection<String> hashes)
       throws OrmException, IOException {
     return byCommitsOnBranchNotMerged(
         repo,
-        db,
         branch,
         hashes,
         // Account for all commit predicates plus ref, project, status.
@@ -181,20 +154,16 @@
 
   @VisibleForTesting
   Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo,
-      ReviewDb db,
-      Branch.NameKey branch,
-      Collection<String> hashes,
-      int indexLimit)
+      Repository repo, Branch.NameKey branch, Collection<String> hashes, int indexLimit)
       throws OrmException, 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)
+      Repository repo, Branch.NameKey branch, Collection<String> hashes)
       throws OrmException, IOException {
     Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
     String lastPrefix = null;
@@ -215,14 +184,13 @@
 
     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(
@@ -307,34 +275,42 @@
     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 {
+    // 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/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 87845d4..b698181 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -95,7 +95,7 @@
     if (user.isIdentifiedUser()) {
       return user.asIdentifiedUser().state().getProjectWatches().keySet();
     }
-    return Collections.<ProjectWatchKey>emptySet();
+    return Collections.emptySet();
   }
 
   protected static List<Predicate<ChangeData>> none() {
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/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index b3e1c27..dcbf517 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 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;
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 17c23b6..09ff687 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -23,7 +23,6 @@
 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;
@@ -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);
   }
@@ -242,7 +244,7 @@
       ChangeData d, Map<Project.NameKey, Repository> repos, Map<Project.NameKey, RevWalk> revWalks)
       throws OrmException, 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/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/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
new file mode 100644
index 0000000..1d49f1e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.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.query.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;
+
+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) throws OrmException {
+    return ChangeField.getDirectories(cd).stream().anyMatch(pattern::run);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 5364a66..0d1ae44 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -15,15 +15,12 @@
 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 +28,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;
 
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 296dc17..d5dc692 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -24,9 +24,11 @@
 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;
@@ -157,11 +159,14 @@
 
   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");
+    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..42b38bf 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -37,7 +37,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
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..479bb52
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -0,0 +1,162 @@
+// 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.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 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);
+    }
+  }
+}
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..d4dc46d
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaBackend.java
@@ -0,0 +1,86 @@
+// 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);
+  }
+}
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..9c55e11
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaEnforcer.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.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);
+
+  /**
+   * 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/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..99a7c1d
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaResponse.java
@@ -0,0 +1,117 @@
+// 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.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 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<String> message();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract QuotaResponse.Builder status(Status status);
+
+    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 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 1df431e..0477a63 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -12,6 +12,7 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
+        "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index c301ab2..35922f4 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -21,9 +21,8 @@
 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;
@@ -35,18 +34,15 @@
 @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;
   }
@@ -55,12 +51,14 @@
   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));
+    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..e47540d 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -109,7 +109,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 4185f36..b704fd1 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -27,7 +27,6 @@
 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,7 +48,9 @@
 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;
@@ -73,7 +73,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 +85,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;
@@ -131,9 +131,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
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 108ee0e..135718b 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;
@@ -74,7 +72,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 +85,6 @@
 
   @Inject
   DeleteDraftComments(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       Factory batchUpdateFactory,
       Provider<ChangeQueryBuilder> queryBuilderProvider,
@@ -99,7 +95,6 @@
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache) {
-    this.db = db;
     this.userProvider = userProvider;
     this.batchUpdateFactory = batchUpdateFactory;
     this.queryBuilderProvider = queryBuilderProvider;
@@ -142,7 +137,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 +146,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,7 +169,7 @@
     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;
     }
@@ -184,12 +179,11 @@
         throws OrmException, 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,7 +191,7 @@
         result.change =
             changeJsonFactory
                 .create(ListChangesOption.SKIP_MERGEABLE)
-                .format(changeDataFactory.create(ctx.getDb(), ctx.getNotes()));
+                .format(changeDataFactory.create(ctx.getNotes()));
         result.deleted = comments.build();
       }
       return dirty;
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/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/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 4bbd8fc..c6e2cb0 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.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.common.UsedAt;
 import com.google.gerrit.extensions.common.HttpPasswordInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,7 +26,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;
@@ -117,7 +117,7 @@
                     ExternalId.createWithPassword(
                         extId.key(), extId.accountId(), extId.email(), newPassword)));
 
-    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/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 2c0512c..f527ba4 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;
@@ -97,7 +98,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(
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 4849698..a109815 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -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
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index ccce998..15dc9e5 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,25 +14,20 @@
 
 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.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;
@@ -43,7 +38,6 @@
 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,25 +47,22 @@
     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;
   }
 
@@ -83,7 +74,7 @@
     // 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;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
index 12b3797..59b3111 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
@@ -18,25 +18,21 @@
 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;
   }
@@ -44,7 +40,7 @@
   @Override
   public IncludedInInfo apply(ChangeResource rsrc)
       throws RestApiException, OrmException, IOException {
-    PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
+    PatchSet ps = psUtil.current(rsrc.getNotes());
     return includedIn.apply(rsrc.getProject(), ps.getRevision().get());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 561c27c..9972195 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;
@@ -43,7 +43,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 +53,6 @@
 
   @Inject
   public ChangesCollection(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> user,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
@@ -62,7 +60,6 @@
       ChangeResource.Factory changeResourceFactory,
       PermissionBackend permissionBackend,
       ProjectCache projectCache) {
-    this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
     this.views = views;
@@ -101,7 +98,8 @@
   }
 
   public ChangeResource parse(Change.Id id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws ResourceConflictException, ResourceNotFoundException, OrmException,
+          PermissionBackendException, IOException {
     List<ChangeNotes> notes = changeFinder.find(id);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
@@ -127,7 +125,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 +137,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/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index b0ebd2b..52334ed 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;
@@ -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(
@@ -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.
@@ -308,31 +306,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();
   }
@@ -345,7 +336,7 @@
       @Nullable Change sourceChange,
       ObjectId sourceCommit,
       CherryPickInput input)
-      throws OrmException, IOException, BadRequestException, ConfigInvalidException {
+      throws OrmException, IOException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
     Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
@@ -355,13 +346,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());
@@ -374,6 +362,12 @@
     return changeId;
   }
 
+  private NotifyResolver.Result resolveNotify(CherryPickInput input)
+      throws BadRequestException, OrmException, 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/Comments.java b/java/com/google/gerrit/server/restapi/change/Comments.java
index f563cc6..22f376b 100644
--- a/java/com/google/gerrit/server/restapi/change/Comments.java
+++ b/java/com/google/gerrit/server/restapi/change/Comments.java
@@ -20,32 +20,27 @@
 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;
   }
 
@@ -65,8 +60,7 @@
     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 fd25658..9395d9d 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;
@@ -76,7 +76,6 @@
 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;
@@ -101,7 +100,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 +113,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 +134,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,7 +152,7 @@
     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;
   }
 
@@ -165,6 +161,35 @@
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
       throws OrmException, 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 +197,222 @@
     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));
+    }
+
     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, OrmException, 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);
-        }
-        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));
-        }
-        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);
+
       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 OrmException, 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);
+    }
+
+    return change;
+  }
+
+  @Nullable
+  private ObjectId getParentCommit(
+      Repository repo,
+      RevWalk revWalk,
+      String inputBranch,
+      @Nullable Boolean newBranch,
+      @Nullable PatchSet basePatchSet,
+      @Nullable String baseCommit)
+      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));
+      }
+
+      RevCommit parentRevCommit = revWalk.parseCommit(parentCommit);
+      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)) {
+          parentCommit = null;
+        } else {
+          throw new BadRequestException("Must provide a destination branch");
+        }
+      }
+    }
+
+    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,
@@ -360,7 +442,7 @@
       MergeInput merge,
       PersonIdent authorIdent,
       String commitMessage)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, OrmException {
     if (Strings.isNullOrEmpty(merge.source)) {
       throw new BadRequestException("merge.source must be non-empty");
     }
@@ -373,8 +455,7 @@
     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,
@@ -387,8 +468,7 @@
         rw);
   }
 
-  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 +477,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..fa8adc0 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;
@@ -51,7 +50,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 +57,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;
@@ -88,7 +84,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();
@@ -112,7 +108,7 @@
     public boolean updateChange(ChangeContext ctx)
         throws ResourceNotFoundException, OrmException, 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 +122,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 1b85d0b..6df490c 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;
@@ -79,7 +78,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 +92,6 @@
 
   @Inject
   CreateMergePatchSet(
-      Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       CommitsCollection commits,
       @GerritPersonIdent PersonIdent myIdent,
@@ -108,7 +105,6 @@
       ChangeFinder changeFinder,
       PermissionBackend permissionBackend) {
     super(retryHelper);
-    this.db = db;
     this.gitManager = gitManager;
     this.commits = commits;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -130,7 +126,7 @@
     // 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 +137,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 +181,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);
         }
@@ -212,11 +207,11 @@
     }
     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);
     }
-    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..907347f 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;
@@ -41,7 +40,6 @@
 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 +47,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 +55,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;
@@ -77,7 +72,7 @@
     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();
@@ -114,14 +109,14 @@
       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
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/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 881d106..adbe0e6 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;
@@ -58,7 +57,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 +65,6 @@
   @Inject
   public DeleteChangeMessage(
       Provider<CurrentUser> userProvider,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       ChangeMessagesUtil changeMessagesUtil,
       AccountLoader.Factory accountLoaderFactory,
@@ -75,7 +72,6 @@
       RetryHelper retryHelper) {
     super(retryHelper);
     this.userProvider = userProvider;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.changeMessagesUtil = changeMessagesUtil;
     this.accountLoaderFactory = accountLoaderFactory;
@@ -97,8 +93,7 @@
     DeleteChangeMessageOp deleteChangeMessageOp =
         new DeleteChangeMessageOp(resource.getChangeMessageIndex(), 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();
     }
 
@@ -109,8 +104,7 @@
 
   private ChangeMessageInfo createUpdatedChangeMessageInfo(Change.Id id, int targetIdx)
       throws OrmException, PermissionBackendException {
-    List<ChangeMessage> messages =
-        changeMessagesUtil.byChange(dbProvider.get(), notesFactory.createChecked(id));
+    List<ChangeMessage> messages = changeMessagesUtil.byChange(notesFactory.createChecked(id));
     ChangeMessage updatedChangeMessage = messages.get(targetIdx);
     AccountLoader accountLoader = accountLoaderFactory.create(true);
     ChangeMessageInfo info = createChangeMessageInfo(updatedChangeMessage, accountLoader);
@@ -145,10 +139,9 @@
     }
 
     @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), targetMessageIdx, 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..e73c4f3 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;
@@ -51,7 +50,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 +58,6 @@
   @Inject
   public DeleteComment(
       Provider<CurrentUser> userProvider,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       RetryHelper retryHelper,
       CommentsUtil commentsUtil,
@@ -68,7 +65,6 @@
       ChangeNotes.Factory notesFactory) {
     super(retryHelper);
     this.userProvider = userProvider;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.commentsUtil = commentsUtil;
     this.commentJson = commentJson;
@@ -91,13 +87,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 +123,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..bcff6e4 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;
@@ -38,7 +37,6 @@
 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 +45,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 +66,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();
@@ -91,19 +85,18 @@
     public boolean updateChange(ChangeContext ctx)
         throws ResourceNotFoundException, OrmException, 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..3a167bf 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;
@@ -59,31 +58,28 @@
 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 +87,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 +105,7 @@
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
-      throws RestApiException, UpdateException, IOException {
+      throws RestApiException, UpdateException, IOException, OrmException, ConfigInvalidException {
     if (input == null) {
       input = new DeleteVoteInput();
     }
@@ -129,7 +124,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(
@@ -169,7 +167,7 @@
             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 +176,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 +203,6 @@
       }
 
       ctx.getUpdate(psId).removeApprovalFor(accountId, label);
-      ctx.getDb().patchSetApprovals().upsert(Collections.singleton(deletedApproval(ctx)));
 
       StringBuilder msg = new StringBuilder();
       msg.append("Removed ");
@@ -218,22 +210,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 +222,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/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index b8e24a5..9f06252 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -21,7 +21,6 @@
 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;
@@ -36,7 +35,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 +42,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;
   }
 
@@ -71,7 +67,7 @@
     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 1bb6bf2..5f2c370 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.common.FileInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -33,7 +34,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;
@@ -113,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;
@@ -124,7 +123,6 @@
 
     @Inject
     ListFiles(
-        Provider<ReviewDb> db,
         Provider<CurrentUser> self,
         FileInfoJson fileInfoJson,
         Revisions revisions,
@@ -132,7 +130,6 @@
         PatchListCache patchListCache,
         PatchSetUtil psUtil,
         PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore) {
-      this.db = db;
       this.self = self;
       this.fileInfoJson = fileInfoJson;
       this.revisions = revisions;
@@ -267,7 +264,7 @@
           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(
@@ -332,7 +329,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/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index a8f8bbb..c8641d5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -14,43 +14,80 @@
 
 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.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 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 void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
+    dynamicBeans.put(plugin, dynamicBean);
+  }
+
+  @Override
+  public DynamicBean getDynamicBean(String plugin) {
+    return dynamicBeans.get(plugin);
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(rsrc));
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
   Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(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/GetContent.java b/java/com/google/gerrit/server/restapi/change/GetContent.java
index 6b9bf17..c133581 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;
@@ -33,7 +32,6 @@
 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 +41,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 +51,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;
@@ -90,7 +85,7 @@
 
   private String getMessage(ChangeNotes notes) throws OrmException, 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);
     }
@@ -106,7 +101,7 @@
 
   private byte[] getMergeList(ChangeNotes notes) throws OrmException, 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..06fdd99 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDetail.java
@@ -18,12 +18,14 @@
 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,6 +49,16 @@
   }
 
   @Override
+  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) throws OrmException {
     return delegate.apply(rsrc);
   }
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/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 3eea736..9a605c0 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;
@@ -44,13 +44,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 +58,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;
@@ -81,7 +79,7 @@
 
   private List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
       throws OrmException, IOException, PermissionBackendException {
-    Set<String> groups = getAllGroups(rsrc.getNotes(), db.get(), psUtil);
+    Set<String> groups = getAllGroups(rsrc.getNotes(), psUtil);
     if (groups.isEmpty()) {
       return Collections.emptyList();
     }
@@ -125,11 +123,9 @@
   }
 
   @VisibleForTesting
-  public static Set<String> getAllGroups(ChangeNotes notes, ReviewDb db, PatchSetUtil psUtil)
+  public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil)
       throws OrmException {
-    return psUtil.byChange(db, notes).stream()
-        .flatMap(ps -> ps.getGroups().stream())
-        .collect(toSet());
+    return psUtil.byChange(notes).stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet());
   }
 
   private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) throws OrmException {
@@ -153,7 +149,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/GetRevisionActions.java b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
index 03b95a6..b0b49a3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
@@ -19,7 +19,6 @@
 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;
@@ -42,19 +41,16 @@
 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;
@@ -72,8 +68,7 @@
     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);
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index a5dd868..f04a3e8 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;
@@ -28,25 +27,17 @@
 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;
   }
@@ -56,7 +47,7 @@
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws IOException, AuthException, OrmException, 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 40f4642..902a67e 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 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;
@@ -31,18 +30,15 @@
 
 @Singleton
 public class ListChangeComments implements RestReadView<ChangeResource> {
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
 
   @Inject
   ListChangeComments(
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil) {
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
@@ -51,12 +47,12 @@
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
       throws AuthException, OrmException, PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return commentJson
         .get()
         .setFillAccounts(true)
         .setFillPatchSet(true)
         .newCommentFormatter()
-        .format(commentsUtil.publishedByChange(db.get(), cd.notes()));
+        .format(commentsUtil.publishedByChange(cd.notes()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index a524f6d..7bdc139 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -18,7 +18,6 @@
 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;
@@ -32,18 +31,15 @@
 
 @Singleton
 public class ListChangeDrafts implements RestReadView<ChangeResource> {
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<CommentJson> commentJson;
   private 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;
@@ -55,9 +51,9 @@
     if (!rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     List<Comment> drafts =
-        commentsUtil.draftByChangeAuthor(db.get(), cd.notes(), rsrc.getUser().getAccountId());
+        commentsUtil.draftByChangeAuthor(cd.notes(), rsrc.getUser().getAccountId());
     return commentJson
         .get()
         .setFillAccounts(false)
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
index 9ce2157..c658fc4 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -19,30 +19,24 @@
 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);
   }
@@ -50,8 +44,7 @@
   @Override
   public List<ChangeMessageInfo> apply(ChangeResource resource)
       throws OrmException, PermissionBackendException {
-    List<ChangeMessage> messages =
-        changeMessagesUtil.byChange(dbProvider.get(), resource.getNotes());
+    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..3ea2ac2 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -17,7 +17,6 @@
 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;
@@ -29,18 +28,15 @@
 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;
@@ -49,7 +45,7 @@
   @Override
   public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
       throws AuthException, OrmException, PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    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..65c8d45 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -18,7 +18,6 @@
 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;
@@ -26,7 +25,6 @@
 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,18 +32,13 @@
 
 @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;
@@ -55,8 +48,7 @@
   public List<ReviewerInfo> apply(ChangeResource rsrc)
       throws OrmException, 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..f10d92b 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
@@ -15,7 +15,6 @@
 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;
@@ -27,9 +26,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
@@ -40,6 +38,6 @@
   @Override
   protected Iterable<Comment> listComments(RevisionResource rsrc) throws OrmException {
     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..3df7e9c 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -18,7 +18,6 @@
 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;
@@ -31,21 +30,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 {
     return commentsUtil.draftByPatchSetAuthor(
-        db.get(), rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
+        rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
   }
 
   protected boolean 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..6e7ffd9 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -19,7 +19,6 @@
 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;
@@ -27,7 +26,6 @@
 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 +33,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;
@@ -60,8 +53,7 @@
     }
 
     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..c13b1e7 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -18,7 +18,6 @@
 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;
@@ -31,14 +30,11 @@
 
 @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;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
index 7c9ba73..49c8fb6 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
@@ -20,14 +20,12 @@
 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;
   }
@@ -67,7 +60,7 @@
   private boolean isReviewed(ChangeResource rsrc) {
     try {
       return changeDataFactory
-          .create(dbProvider.get(), rsrc.getNotes())
+          .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
     } catch (OrmException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
index 6e15dcc..0651e7b 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
@@ -19,14 +19,12 @@
 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;
   }
@@ -66,7 +59,7 @@
   private boolean isReviewed(ChangeResource rsrc) {
     try {
       return changeDataFactory
-          .create(dbProvider.get(), rsrc.getNotes())
+          .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
     } catch (OrmException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index b196347..4441397 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -24,8 +24,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;
@@ -40,10 +38,8 @@
 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());
@@ -103,14 +96,14 @@
     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())) {
@@ -157,8 +150,7 @@
       ObjectId commit,
       Ref ref,
       SubmitType submitType,
-      String strategy)
-      throws OrmException {
+      String strategy) {
     if (commit == null) {
       return false;
     }
@@ -186,25 +178,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..36f91bb 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -27,19 +27,18 @@
 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 +46,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;
@@ -68,6 +68,7 @@
 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,6 +110,7 @@
     this.psUtil = psUtil;
     this.approvalsUtil = approvalsUtil;
     this.projectCache = projectCache;
+    this.moveEnabled = gerritConfig.getBoolean("change", null, "move", true);
   }
 
   @Override
@@ -117,6 +118,12 @@
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
       throws RestApiException, OrmException, 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 +132,7 @@
     }
     input.destinationBranch = RefNames.fullName(input.destinationBranch);
 
-    if (change.getStatus().isClosed()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     }
 
@@ -139,16 +146,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();
     }
@@ -174,7 +180,7 @@
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, ResourceConflictException, IOException {
       change = ctx.getChange();
-      if (change.getStatus() != Status.NEW) {
+      if (!change.isNew()) {
         throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
       }
 
@@ -190,8 +196,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 +245,7 @@
       }
       ChangeMessage cmsg =
           ChangeMessagesUtil.newMessage(ctx, msgBuf.toString(), ChangeMessagesUtil.TAG_MOVE);
-      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+      cmUtil.addChangeMessage(update, cmsg);
 
       return true;
     }
@@ -257,7 +262,7 @@
       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 +280,6 @@
                 (short) 0,
                 ctx.getWhen()));
       }
-      // Remove votes from ReviewDb.
-      ctx.getDb().patchSetApprovals().upsert(approvals);
     }
   }
 
@@ -289,7 +292,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       return description;
     }
 
@@ -316,6 +319,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 845f584..7658f34 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;
@@ -126,7 +121,6 @@
 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 +154,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 +167,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 +176,6 @@
 
   @Inject
   PostReview(
-      Provider<ReviewDb> db,
       RetryHelper retryHelper,
       ChangeResource.Factory changeResourceFactory,
       ChangeData.Factory changeDataFactory,
@@ -199,14 +190,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 +209,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;
@@ -260,20 +248,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 +262,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 +286,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 +296,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 +304,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 +329,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 +340,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 +406,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 +419,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,15 +434,7 @@
       }
     }
     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)
@@ -476,7 +449,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();
@@ -506,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()));
@@ -867,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;
@@ -878,15 +846,10 @@
     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
@@ -895,7 +858,7 @@
             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);
@@ -909,18 +872,10 @@
       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(
@@ -989,7 +944,7 @@
           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();
     }
@@ -1077,7 +1032,7 @@
     }
 
     private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) throws OrmException {
-      return commentsUtil.publishedByChange(ctx.getDb(), ctx.getNotes()).stream()
+      return commentsUtil.publishedByChange(ctx.getNotes()).stream()
           .map(CommentSetEntry::create)
           .collect(toSet());
     }
@@ -1090,8 +1045,7 @@
 
     private Map<String, Comment> changeDrafts(ChangeContext ctx) throws OrmException {
       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);
       }
@@ -1101,8 +1055,7 @@
     private Map<String, Comment> patchSetDrafts(ChangeContext ctx) throws OrmException {
       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;
@@ -1151,7 +1104,7 @@
       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;
@@ -1166,7 +1119,7 @@
       // 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;
       }
 
@@ -1235,14 +1188,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();
     }
 
@@ -1253,11 +1198,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");
       }
 
@@ -1293,11 +1238,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()) {
@@ -1361,7 +1303,6 @@
 
       for (PatchSetApproval a :
           approvalsUtil.byPatchSetUser(
-              ctx.getDb(),
               ctx.getNotes(),
               psId,
               user.getAccountId(),
@@ -1381,7 +1322,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();
@@ -1405,7 +1346,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..4aeb07f 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;
@@ -33,7 +34,6 @@
 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,19 +43,19 @@
     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;
   }
 
@@ -68,22 +68,30 @@
       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, OrmException, 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 18e86d1..08d86da 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;
@@ -61,7 +60,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 +72,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);
@@ -103,7 +99,7 @@
     }
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
     }
     if (!rsrc.getUser().isIdentifiedUser()) {
@@ -116,14 +112,13 @@
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
       throws OrmException, 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())
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index 3d401c4..b0cad84 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;
@@ -39,18 +42,18 @@
 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;
   }
 
@@ -73,8 +76,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 7878ce5..a7dcc12 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;
@@ -43,7 +41,6 @@
 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 +51,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 +60,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;
@@ -89,14 +83,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 +94,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();
@@ -124,7 +114,7 @@
     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..b5c6c95 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;
@@ -37,26 +36,18 @@
 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 +60,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();
     }
@@ -93,10 +83,9 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx) throws OrmException {
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       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 +98,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..974d715 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;
@@ -53,7 +52,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 +60,6 @@
 
   @Inject
   PutDraftComment(
-      Provider<ReviewDb> db,
       DeleteDraftComment delete,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
@@ -70,7 +67,6 @@
       Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
     super(retryHelper);
-    this.db = db;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -93,8 +89,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();
@@ -118,7 +113,7 @@
     public boolean updateChange(ChangeContext ctx)
         throws ResourceNotFoundException, OrmException, 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 +128,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 +136,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..39dfc7f 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;
@@ -68,12 +67,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 +79,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;
   }
 
@@ -105,7 +101,7 @@
       BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           OrmException, ConfigInvalidException {
-    PatchSet ps = psUtil.current(db.get(), resource.getNotes());
+    PatchSet ps = psUtil.current(resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
     }
@@ -121,11 +117,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 +129,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 +140,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 +148,16 @@
     return Response.ok("ok");
   }
 
+  private NotifyResolver.Result resolveNotify(CommitMessageInput input, ChangeResource resource)
+      throws BadRequestException, OrmException, 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,
@@ -188,7 +187,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..bc272e9 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;
@@ -40,24 +39,17 @@
 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 +74,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();
     }
@@ -123,7 +114,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 d1eea44..6176ea0 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;
@@ -70,7 +71,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 +83,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);
@@ -137,7 +143,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 2e2f565..afb674b 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -27,13 +27,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;
@@ -51,7 +50,6 @@
 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;
@@ -74,7 +72,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;
@@ -86,7 +83,6 @@
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       PatchSetUtil patchSetUtil) {
@@ -95,7 +91,6 @@
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
     this.json = json;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.patchSetUtil = patchSetUtil;
@@ -109,7 +104,7 @@
     // 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();
@@ -118,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(),
@@ -161,18 +157,15 @@
 
     Base 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);
     }
     PatchSet.Id baseId = base.patchSet().getId();
     if (change.getId().equals(baseId.getParentKey())) {
       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())) {
@@ -181,7 +174,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(
@@ -213,7 +206,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!(change.getStatus().isOpen() && rsrc.isCurrent())) {
+    if (!(change.isNew() && rsrc.isCurrent())) {
       return description;
     }
 
@@ -243,13 +236,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;
@@ -272,7 +269,7 @@
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
         throws OrmException, UpdateException, RestApiException, IOException,
             PermissionBackendException {
-      PatchSet ps = psUtil.current(rebase.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/Rebuild.java b/java/com/google/gerrit/server/restapi/change/Rebuild.java
deleted file mode 100644
index dc390cc..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()));
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 51f70bd..8634e1d 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;
@@ -38,7 +37,6 @@
 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,18 +59,15 @@
 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;
   }
 
@@ -235,7 +230,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..40ef867 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -25,7 +25,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;
@@ -48,7 +47,6 @@
 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;
@@ -93,13 +88,12 @@
     // 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);
@@ -119,18 +113,18 @@
     @Override
     public boolean updateChange(ChangeContext ctx) throws OrmException, 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;
     }
 
@@ -168,7 +162,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (change.getStatus() != Status.ABANDONED) {
+    if (!change.isAbandoned()) {
       return description;
     }
 
@@ -192,7 +186,7 @@
       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..9937160 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;
@@ -92,7 +90,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 +103,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 +121,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,7 +136,7 @@
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
     this.projectCache = projectCache;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
   }
 
   @Override
@@ -150,7 +145,7 @@
       throws IOException, OrmException, 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));
     }
 
@@ -168,7 +163,7 @@
     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 +215,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 +236,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 +265,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 +275,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 +287,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 +304,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 +312,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/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index ece52bf..1d4d4dd 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;
@@ -51,7 +50,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 +78,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 +86,6 @@
       PluginMapContext<ReviewerSuggestion> reviewerSuggestionPluginMap,
       Provider<InternalChangeQuery> queryProvider,
       @FanOutExecutor ExecutorService executor,
-      Provider<ReviewDb> dbProvider,
       ApprovalsUtil approvalsUtil,
       @GerritServerConfig Config config) {
     this.changeQueryBuilder = changeQueryBuilder;
@@ -97,7 +93,6 @@
     this.queryProvider = queryProvider;
     this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
     this.executor = executor;
-    this.dbProvider = dbProvider;
     this.approvalsUtil = approvalsUtil;
   }
 
@@ -183,7 +178,7 @@
 
       // Remove existing reviewers
       approvalsUtil
-          .getReviewers(dbProvider.get(), changeNotes)
+          .getReviewers(changeNotes)
           .byState(REVIEWER)
           .forEach(
               r -> {
@@ -194,7 +189,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());
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index f0aef13..cf69080 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -23,14 +23,12 @@
 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 +37,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 +44,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;
@@ -99,6 +94,6 @@
   }
 
   private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) throws OrmException {
-    return approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
+    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 1c97431..343afc3 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -34,6 +34,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;
@@ -57,7 +58,6 @@
 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;
@@ -399,7 +399,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..60c9a54 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -24,14 +24,12 @@
 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 +38,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 +45,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;
@@ -89,8 +84,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..84257e9 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;
@@ -36,7 +35,6 @@
 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 +46,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 +54,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;
@@ -85,9 +80,9 @@
       throws ResourceNotFoundException, AuthException, OrmException, 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 +109,6 @@
       permissionBackend
           .user(change.getUser())
           .change(change.getNotes())
-          .database(dbProvider)
           .check(ChangePermission.READ);
       return projectCache.checkedGet(change.getProject()).statePermitsRead();
     } catch (AuthException e) {
@@ -135,7 +129,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));
         }
@@ -151,10 +145,7 @@
   private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id)
       throws OrmException {
     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/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 773d12d..fdfa799 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -36,7 +36,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;
@@ -107,7 +106,6 @@
     }
   }
 
-  private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
@@ -128,7 +126,6 @@
 
   @Inject
   Submit(
-      Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
@@ -140,7 +137,6 @@
       Provider<InternalChangeQuery> queryProvider,
       PatchSetUtil psUtil,
       ProjectCache projectCache) {
-    this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
@@ -199,7 +195,7 @@
       throws OrmException, 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 +208,21 @@
     }
 
     try (MergeOp op = mergeOpProvider.get()) {
-      ReviewDb db = dbProvider.get();
-      op.merge(db, change, submitter, true, input, false);
+      op.merge(change, submitter, true, input, false);
       try {
-        change =
-            changeNotesFactory.createChecked(db, change.getProject(), change.getId()).getChange();
+        change = changeNotesFactory.createChecked(change.getProject(), change.getId()).getChange();
       } catch (NoSuchChangeException e) {
         throw new ResourceConflictException("change is deleted");
       }
     }
 
-    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));
+    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));
   }
 
   /**
@@ -262,7 +253,6 @@
         Set<ChangePermission> can =
             permissionBackend
                 .user(user)
-                .database(dbProvider)
                 .change(c)
                 .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT));
         if (!can.contains(ChangePermission.READ)) {
@@ -307,7 +297,7 @@
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     Change change = resource.getChange();
-    if (!change.getStatus().isOpen()
+    if (!change.isNew()
         || change.isWorkInProgress()
         || !resource.isCurrent()
         || !resource.permissions().testOrFalse(ChangePermission.SUBMIT)) {
@@ -323,8 +313,7 @@
       throw new OrmRuntimeException("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) {
@@ -336,7 +325,7 @@
 
     ChangeSet cs;
     try {
-      cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getUser());
+      cs = mergeSuperSet.get().completeChangeSet(cd.change(), resource.getUser());
     } catch (OrmException | IOException | PermissionBackendException e) {
       throw new OrmRuntimeException(
           "Could not determine complete set of changes to be submitted", e);
@@ -456,8 +445,7 @@
       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);
       }
     }
@@ -467,18 +455,15 @@
   private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
       throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException,
           IOException, ConfigInvalidException {
-    PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider);
+    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()));
@@ -495,18 +480,12 @@
   }
 
   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;
@@ -516,7 +495,7 @@
     public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
         throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
             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..39413b6 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -21,14 +21,12 @@
 import com.google.common.flogger.FluentLogger;
 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;
@@ -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;
@@ -128,12 +123,11 @@
       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 {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 0ce5750..708d914 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;
@@ -54,13 +52,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;
@@ -83,17 +80,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..7cba8e7 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;
@@ -38,13 +37,11 @@
 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 +54,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;
@@ -95,7 +90,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..684d22f 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -23,7 +23,6 @@
 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;
@@ -31,11 +30,9 @@
 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 +42,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;
@@ -74,7 +69,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) {
diff --git a/java/com/google/gerrit/server/restapi/change/Votes.java b/java/com/google/gerrit/server/restapi/change/Votes.java
index 3b2548c..a5d77e9 100644
--- a/java/com/google/gerrit/server/restapi/change/Votes.java
+++ b/java/com/google/gerrit/server/restapi/change/Votes.java
@@ -23,13 +23,11 @@
 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;
@@ -66,12 +64,10 @@
 
   @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;
     }
 
@@ -85,7 +81,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/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index eca26c3..43bfa81 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,7 +64,6 @@
 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;
@@ -94,10 +90,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 +112,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 +131,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,7 +145,7 @@
     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();
@@ -308,11 +298,6 @@
     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 +310,6 @@
     return CharMatcher.is('/').trimTrailingFrom(docUrl) + '/';
   }
 
-  private boolean isNoteDbEnabled() {
-    return migration.readChanges();
-  }
-
   private PluginConfigInfo getPluginInfo() {
     PluginConfigInfo info = new PluginConfigInfo();
     info.hasAvatars = toBoolean(avatar.hasImplementation());
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/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..2feb580 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
@@ -14,10 +14,12 @@
 
 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;
@@ -25,19 +27,17 @@
 
 @Singleton
 class ListTopMenus implements RestReadView<ConfigResource> {
-  private final DynamicSet<TopMenu> extensions;
+  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 bdf1c74..2d2d02b 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -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;
@@ -144,19 +145,18 @@
   }
 
   Account findAccount(String nameOrEmailOrId)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+      throws UnprocessableEntityException, OrmException, 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();
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 0572114..e6bd791 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -36,7 +36,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,6 +49,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.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;
@@ -156,7 +156,7 @@
       args.initialMembers =
           ownerUuid == null
               ? Collections.singleton(self.get().getAccountId())
-              : Collections.<Account.Id>emptySet();
+              : Collections.emptySet();
     }
 
     try {
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index d197cb8..fca88e7 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -69,8 +69,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 {
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 8d8dc3d..600379c 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.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;
@@ -202,7 +203,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")
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 d407f69..dbc124b 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -72,7 +72,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/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index fa9285d..6a7262b 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;
@@ -82,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
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 1664635..51e0304 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;
@@ -75,20 +74,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 +107,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 +115,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/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
index de2ac64..3dafb39 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.project.BranchResource;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.lib.Config;
@@ -76,7 +77,7 @@
 
   @Override
   public MergeableInfo apply(BranchResource resource)
-      throws IOException, BadRequestException, ResourceNotFoundException {
+      throws IOException, OrmException, BadRequestException, ResourceNotFoundException {
     if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
         || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
       throw new BadRequestException("Submit type: " + submitType + " is not supported");
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 15cd824..e8a12bc 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;
@@ -39,14 +41,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;
@@ -74,7 +75,7 @@
 
   @Override
   public CommitResource parse(ProjectResource parent, IdString id)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, OrmException {
     parent.getProjectState().checkStatePermitsRead();
     ObjectId objectId;
     try {
@@ -105,23 +106,28 @@
   }
 
   /** @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 OrmException, 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 feb37c0..c3bbb60 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -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;
@@ -64,10 +63,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,19 +75,19 @@
       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
@@ -117,7 +116,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 +149,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 b92fba8..6844cac 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,29 +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.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;
@@ -79,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 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 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;
@@ -224,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()));
@@ -240,93 +187,6 @@
     }
   }
 
-  private 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.");
-    } catch (RepositoryNotFoundException badName) {
-      throw new BadRequestException("invalid project name: " + nameKey);
-    } 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);
@@ -347,78 +207,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);
-      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/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 3f7d406..559e8e1 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -30,13 +30,11 @@
 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 +93,7 @@
   private final MetaDataUpdate.Server metaDataUpdateFactory;
   private final GroupBackend groupBackend;
   private final WebLinks webLinks;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   public GetAccess(
@@ -105,7 +104,8 @@
       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 +114,7 @@
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.groupBackend = groupBackend;
     this.webLinks = webLinks;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   public ProjectAccessInfo apply(Project.NameKey nameKey)
@@ -141,18 +142,14 @@
 
     ProjectConfig config;
     try (MetaDataUpdate md = metaDataUpdateFactory.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)) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetHead.java b/java/com/google/gerrit/server/restapi/project/GetHead.java
index bc267c8..5b10120 100644
--- a/java/com/google/gerrit/server/restapi/project/GetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/GetHead.java
@@ -23,6 +23,7 @@
 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;
@@ -53,7 +54,8 @@
 
   @Override
   public String apply(ProjectResource rsrc)
-      throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException {
+      throws AuthException, ResourceNotFoundException, IOException, OrmException,
+          PermissionBackendException {
     rsrc.getProjectState().statePermitsRead();
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       Ref head = repo.getRefDatabase().exactRef(Constants.HEAD);
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index eccf29e..dc7e537 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -37,11 +37,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;
@@ -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 {
@@ -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 f59e984..98c8c61 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 76ea0c9..150989b 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 1c74021..cd0bd5c 100644
--- a/java/com/google/gerrit/server/restapi/project/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/PutDescription.java
@@ -42,15 +42,18 @@
   private final ProjectCache cache;
   private final MetaDataUpdate.Server updateFactory;
   private final PermissionBackend permissionBackend;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   PutDescription(
       ProjectCache cache,
       MetaDataUpdate.Server updateFactory,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectConfig.Factory projectConfigFactory) {
     this.cache = cache;
     this.updateFactory = updateFactory;
     this.permissionBackend = permissionBackend;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
@@ -68,7 +71,7 @@
         .check(ProjectPermission.WRITE_CONFIG);
 
     try (MetaDataUpdate md = updateFactory.create(resource.getNameKey())) {
-      ProjectConfig config = ProjectConfig.read(md);
+      ProjectConfig config = projectConfigFactory.read(md);
       Project project = config.getProject();
       project.setDescription(Strings.emptyToNull(input.description));
 
@@ -85,7 +88,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());
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 c9d69a5..19e89a9 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -56,6 +56,7 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final SetAccessUtil accessUtil;
   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
+  private final ProjectConfig.Factory projectConfigFactory;
 
   @Inject
   private SetAccess(
@@ -66,7 +67,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,6 +77,7 @@
     this.identifiedUser = identifiedUser;
     this.accessUtil = accessUtil;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+    this.projectConfigFactory = projectConfigFactory;
   }
 
   @Override
@@ -89,7 +92,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/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index ef292e5..0f346df 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 d02d04a..12aaf76 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -61,6 +61,7 @@
   private final MetaDataUpdate.Server updateFactory;
   private final AllProjectsName allProjects;
   private final AllUsersName allUsers;
+  private final ProjectConfig.Factory projectConfigFactory;
   private volatile boolean allowProjectOwnersToChangeParent;
 
   @Inject
@@ -70,12 +71,14 @@
       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);
   }
@@ -97,7 +100,7 @@
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
     validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin);
     try (MetaDataUpdate md = updateFactory.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/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index f7c25a4..f4b7254 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -71,7 +71,7 @@
       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();
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 568fa4e..a327d6e 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;
@@ -172,6 +173,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;
@@ -186,6 +188,7 @@
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         GitRepositoryManager repositoryManager,
+        PluginConfigFactory pluginConfigFactory,
         PatchListCache patchListCache,
         PatchSetInfoFactory patchSetInfoFactory,
         IdentifiedUser.GenericFactory userFactory,
@@ -196,6 +199,7 @@
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.repositoryManager = repositoryManager;
+      this.pluginConfigFactory = pluginConfigFactory;
       this.patchListCache = patchListCache;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.userFactory = userFactory;
@@ -236,6 +240,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..edc2674 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -159,7 +159,7 @@
       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 8b9cfe3..1c28c54 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -21,13 +21,13 @@
 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;
@@ -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);
 
@@ -110,6 +109,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..1b44f39 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -22,39 +22,28 @@
 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.gwtorm.server.OrmException;
 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 +61,40 @@
   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, OrmException {
     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 +104,131 @@
     }
   }
 
-  private void initAllProjects(Repository git) throws IOException, ConfigInvalidException {
+  private void initAllProjects(Repository git, AllProjectsInput input)
+      throws IOException, ConfigInvalidException, OrmException {
     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 a04def6..93cad19 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
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/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..fa4de40 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -17,6 +17,7 @@
 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;
@@ -32,6 +33,7 @@
 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 +49,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 +116,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;
   }
@@ -352,4 +356,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
index dddf23a..9f6822c 100644
--- a/java/com/google/gerrit/server/schema/JdbcUtil.java
+++ b/java/com/google/gerrit/server/schema/JdbcUtil.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.server.UsedAt;
+import com.google.gerrit.common.UsedAt;
 
 public class JdbcUtil {
 
@@ -28,6 +28,7 @@
     return hostname;
   }
 
+  // TODO(dborowitz): Still used by plugins post-ReviewDb?
   @UsedAt(UsedAt.Project.PLUGINS_ALL)
   public static String port(String port) {
     if (port != null && !port.isEmpty()) {
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/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..f8a9cf7
--- /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.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.gwtorm.server.OrmException;
+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) throws OrmException {
+    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 OrmException(
+            String.format("Failed to upgrade to schema version %d", nextVersion), e);
+      }
+    }
+  }
+
+  private void ensureSchemaCreated() throws OrmException {
+    try {
+      schemaCreator.ensureCreated();
+    } catch (IOException | ConfigInvalidException e) {
+      throw new OrmException("Cannot initialize Gerrit site");
+    }
+  }
+
+  // Config#getEnum requires this to be public, so give it an off-putting name.
+  public enum PrimaryStorageFor216Compatibility {
+    REVIEW_DB,
+    NOTE_DB
+  }
+
+  private void checkNoteDbConfigFor216() throws OrmException {
+    // 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 OrmException(
+          "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 OrmException(
+            "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 OrmException("Failed to check NoteDb migration state", e);
+    }
+  }
+
+  @VisibleForTesting
+  static ImmutableList<Integer> requiredUpgrades(
+      int currentVersion, ImmutableSortedSet<Integer> allVersions) throws OrmException {
+    int firstVersion = allVersions.first();
+    int latestVersion = allVersions.last();
+    if (currentVersion == latestVersion) {
+      return ImmutableList.of();
+    } else if (currentVersion > latestVersion) {
+      throw new OrmException(
+          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 OrmException(
+            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..75a1de2
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
@@ -0,0 +1,42 @@
+// 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.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;
+
+    @Inject
+    Arguments(GitRepositoryManager repoManager, AllProjectsName allProjects) {
+      this.repoManager = repoManager;
+      this.allProjects = allProjects;
+    }
+  }
+
+  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..63bffec
--- /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.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.server.OrmException;
+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 (OrmException 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..da5f7b9
--- /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.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.gwtorm.server.OrmException;
+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() throws OrmException {
+    try (Repository repo = repoManager.openRepository(allProjectsName)) {
+      return IntBlob.parse(repo, REFS_VERSION).map(IntBlob::value).orElse(0);
+    } catch (IOException e) {
+      throw new OrmException("Failed to read " + REFS_VERSION, e);
+    }
+  }
+
+  public void init() throws IOException, OrmException {
+    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 OrmException(
+            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, OrmException {
+    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 OrmException(
+            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..3f7c989
--- /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)
+          .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/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index c25b846..483e363 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.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) {
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..8cf8fe7 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,32 @@
 
 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 OrmException an error occurred.
+   * @throws IOException an error occurred.
+   * @throws ConfigInvalidException an error occurred.
+   */
+  void create() throws OrmException, 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 OrmException an error occurred.
+   * @throws IOException an error occurred.
+   * @throws ConfigInvalidException an error occurred.
+   */
+  void ensureCreated() throws OrmException, 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..3605dd5
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -0,0 +1,228 @@
+// 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.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.gwtorm.server.OrmDuplicateKeyException;
+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.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 OrmException, 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 OrmException, IOException, ConfigInvalidException {
+    try {
+      repoManager.openRepository(allProjectsName).close();
+    } catch (RepositoryNotFoundException e) {
+      create();
+    }
+  }
+
+  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();
+  }
+}
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 5bb3669..0000000
--- a/java/com/google/gerrit/server/schema/Schema_106.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.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()));
-      }
-    }
-  }
-}
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 66f2177..0000000
--- a/java/com/google/gerrit/server/schema/Schema_130.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.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_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 (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_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 503ed7b..0000000
--- a/java/com/google/gerrit/server/schema/Schema_146.java
+++ /dev/null
@@ -1,196 +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.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.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
-import java.util.HashMap;
-import java.util.Map;
-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.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.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;
-
-  @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 {
-    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 : scanAccounts(db).entrySet()) {
-        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());
-        }
-      }
-    } catch (IOException e) {
-      throw new OrmException("Failed to rewrite user branches.", e);
-    }
-  }
-
-  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) throws SQLException {
-    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));
-      }
-      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_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..c520f43
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/testing/BUILD
@@ -0,0 +1,14 @@
+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..9b76b9e
--- /dev/null
+++ b/java/com/google/gerrit/server/securestore/testing/BUILD
@@ -0,0 +1,11 @@
+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/submit/ChangeSet.java b/java/com/google/gerrit/server/submit/ChangeSet.java
index 8c94a21..422e1b9 100644
--- a/java/com/google/gerrit/server/submit/ChangeSet.java
+++ b/java/com/google/gerrit/server/submit/ChangeSet.java
@@ -61,14 +61,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() {
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index 182c22a..f50525c 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -97,8 +97,8 @@
       // 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);
@@ -157,15 +157,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..5ac7c98 100644
--- a/java/com/google/gerrit/server/submit/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
@@ -16,7 +16,9 @@
 
 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;
@@ -88,15 +90,18 @@
   }
 
   public static String createMissingDependencyMessage(
-      Provider<InternalChangeQuery> queryProvider, String commit, String otherCommit)
+      @Nullable CurrentUser caller,
+      Provider<InternalChangeQuery> queryProvider,
+      String commit,
+      String otherCommit)
       throws OrmException {
     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..832c3879 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -28,7 +28,6 @@
 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,6 +36,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.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;
@@ -87,25 +87,28 @@
 
   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)
+      MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
       throws OrmException, 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,10 +150,11 @@
 
       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);
@@ -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.
@@ -206,24 +210,36 @@
     return str.type;
   }
 
-  private List<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
+  private ChangeSet byCommitsOnBranchNotMerged(
+      OpenRepo or, Branch.NameKey branch, Set<String> visibleHashes, Set<String> nonVisibleHashes)
       throws OrmException, 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 OrmException, 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;
   }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 572da0b..8b93ce8 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,28 @@
 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.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;
@@ -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;
@@ -383,12 +382,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");
@@ -425,7 +422,6 @@
    * 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
@@ -436,7 +432,6 @@
    * @throws IOException an error occurred reading from NoteDb.
    */
   public void merge(
-      ReviewDb db,
       Change change,
       IdentifiedUser caller,
       boolean checkSubmitRules,
@@ -445,11 +440,12 @@
       throws OrmException, 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 +455,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",
@@ -534,18 +530,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);
   }
 
@@ -605,7 +599,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 +663,6 @@
         SubmitStrategy strategy =
             submitStrategyFactory.create(
                 submitting.submitType(),
-                db,
                 or.rw,
                 or.canMergeFlag,
                 getAlreadyAccepted(or, ob.oldTip),
@@ -680,7 +673,6 @@
                 commitStatus,
                 submissionId,
                 submitInput,
-                accountsToNotify,
                 submoduleOp,
                 dryrun);
         strategies.add(strategy);
@@ -904,14 +896,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 +916,7 @@
                           change.getLastUpdatedOn(),
                           ChangeMessagesUtil.TAG_MERGED,
                           "Project was deleted.");
-                  cmUtil.addChangeMessage(
-                      ctx.getDb(), ctx.getUpdate(change.currentPatchSetId()), msg);
+                  cmUtil.addChangeMessage(ctx.getUpdate(change.currentPatchSetId()), msg);
 
                   return true;
                 }
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..dbdb0b4 100644
--- a/java/com/google/gerrit/server/submit/MergeSorter.java
+++ b/java/com/google/gerrit/server/submit/MergeSorter.java
@@ -14,6 +14,8 @@
 
 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;
@@ -29,6 +31,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 +39,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;
@@ -70,7 +75,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 7a600a1..87ce6d4 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;
@@ -93,7 +92,7 @@
     return this;
   }
 
-  public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
+  public ChangeSet completeChangeSet(Change change, CurrentUser user)
       throws IOException, OrmException, PermissionBackendException {
     try {
       if (orm == null) {
@@ -110,7 +109,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.
@@ -120,10 +119,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) {
@@ -137,9 +136,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.
@@ -147,11 +145,7 @@
    * @return the resulting larger {@link ChangeSet}
    */
   private ChangeSet topicClosure(
-      ReviewDb db,
-      ChangeSet changeSet,
-      CurrentUser user,
-      Set<String> topicsSeen,
-      Set<String> visibleTopicsSeen)
+      ChangeSet changeSet, CurrentUser user, Set<String> topicsSeen, Set<String> visibleTopicsSeen)
       throws OrmException, PermissionBackendException, IOException {
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
@@ -163,7 +157,7 @@
         continue;
       }
       for (ChangeData topicCd : byTopicOpen(topic)) {
-        if (canRead(db, user, topicCd)) {
+        if (canRead(user, topicCd)) {
           visibleChanges.add(topicCd);
         } else {
           nonVisibleChanges.add(topicCd);
@@ -186,23 +180,22 @@
     return new ChangeSet(visibleChanges, nonVisibleChanges);
   }
 
-  private ChangeSet completeChangeSetIncludingTopics(
-      ReviewDb db, ChangeSet changeSet, CurrentUser user)
+  private ChangeSet completeChangeSetIncludingTopics(ChangeSet changeSet, CurrentUser user)
       throws IOException, OrmException, 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;
@@ -212,7 +205,7 @@
     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()) {
@@ -220,7 +213,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..a487c95 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
@@ -15,7 +15,6 @@
 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;
@@ -41,13 +40,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)
+  ChangeSet completeWithoutTopic(MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
       throws OrmException, IOException, PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index 32f3558..ccb08da 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -16,7 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 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;
@@ -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;
@@ -85,7 +88,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,8 +125,7 @@
       // 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;
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index cf3a44e..8abef03 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -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);
@@ -170,14 +170,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
@@ -186,6 +185,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 {
@@ -227,15 +227,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);
       }
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 8a4fbfb..1cc0db5 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -17,10 +17,10 @@
 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.reviewdb.client.Account;
@@ -32,8 +32,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;
@@ -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();
@@ -301,12 +298,12 @@
       throws IOException, OrmException {
     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,14 +314,7 @@
     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)
@@ -344,7 +334,7 @@
     // 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);
     }
   }
@@ -355,7 +345,7 @@
     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 +361,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 +394,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 =
@@ -483,9 +451,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 +461,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 +501,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..27a6aee 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;
@@ -27,26 +30,34 @@
 import com.google.common.collect.Multiset;
 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.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.gwtorm.server.OrmException;
 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 +67,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 +77,108 @@
 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 {
+      @SuppressWarnings("deprecation")
+      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> 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);
+      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 (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 +200,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 +349,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 +373,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 +408,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 +443,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 +452,165 @@
     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 OrmException, IOException {
+      BatchUpdate.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.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 +619,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 +628,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 3e33677..0000000
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.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.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);
-    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 c67ee8a..0000000
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ /dev/null
@@ -1,841 +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) {
-            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/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..06e93f8 100644
--- a/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -17,7 +17,6 @@
 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.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/AbstractGitCommand.java b/java/com/google/gerrit/sshd/AbstractGitCommand.java
index c49ae82..d726ef6 100644
--- a/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.SshScope.Context;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.apache.sshd.server.Environment;
@@ -84,7 +85,7 @@
     return n;
   }
 
-  private void service() throws IOException, PermissionBackendException, Failure {
+  private void service() throws IOException, OrmException, PermissionBackendException, Failure {
     project = projectState.getProject();
     projectName = project.getNameKey();
 
@@ -101,5 +102,6 @@
     }
   }
 
-  protected abstract void runImpl() throws IOException, PermissionBackendException, Failure;
+  protected abstract void runImpl()
+      throws IOException, OrmException, PermissionBackendException, Failure;
 }
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 8e1f112..ac73482 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -6,6 +6,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//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",
@@ -20,7 +21,6 @@
         "//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",
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index 7a9f298..c8586f2 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -18,7 +18,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;
@@ -40,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;
 
@@ -48,12 +46,10 @@
   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;
   }
@@ -97,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,7 +117,7 @@
   }
 
   private List<ChangeNotes> changeFromNotesFactory(String id) throws OrmException, UnloggedFailure {
-    return changeNotesFactory.create(db, parseId(id));
+    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 ef356f1..d703316 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.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
@@ -100,7 +95,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;
@@ -274,14 +268,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;
@@ -293,7 +284,7 @@
           }
         });
     setGlobalRequestHandlers(
-        Arrays.<RequestHandler<ConnectionService>>asList(
+        Arrays.asList(
             new KeepAliveHandler(),
             new NoMoreSessionsHandler(),
             new TcpipForwardHandler(),
@@ -654,7 +645,7 @@
   }
 
   private void initSubsystems() {
-    setSubsystemFactories(Collections.<NamedFactory<Command>>emptyList());
+    setSubsystemFactories(Collections.emptyList());
   }
 
   private void initUserAuth(
@@ -721,10 +712,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 {}
 
@@ -782,8 +771,6 @@
               public Set<String> supportedFileAttributeViews() {
                 return null;
               }
-            };
-          }
-        });
+            });
   }
 }
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index acdc958..c5f190e 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);
 
     listener().toInstance(registerInParentInjectors());
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/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/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/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 2c15e78..3d9ef56 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -15,10 +15,10 @@
 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.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;
@@ -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 (OrmException | 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(user)
                 .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");
-    } catch (IOException | OrmException e) {
+    } catch (IOException e) {
       throw die("Error opening: '" + projectName);
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/PatchSetParser.java b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
index 8dee69ee..d174561 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;
@@ -37,7 +36,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 +43,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;
@@ -107,7 +103,7 @@
         throw error("\"" + token + "\" is not a valid patch set");
       }
       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");
       }
@@ -129,11 +125,11 @@
   private ChangeNotes getNotes(@Nullable ProjectState projectState, Change.Id changeId)
       throws OrmException, 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");
     }
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 bc8ef2a..fa2d894 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -30,8 +30,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;
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/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..26a0d15 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.restapi.change.AllowedFormats;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -164,7 +165,7 @@
   }
 
   @Override
-  protected void runImpl() throws IOException, PermissionBackendException, Failure {
+  protected void runImpl() throws IOException, OrmException, PermissionBackendException, Failure {
     PacketLineOut packetOut = new PacketLineOut(out);
     packetOut.setFlushOnEnd(true);
     packetOut.writeString("ACK");
@@ -238,13 +239,14 @@
                   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();
   }
 
-  private boolean canRead(ObjectId revId) throws IOException, PermissionBackendException {
+  private boolean canRead(ObjectId revId)
+      throws IOException, OrmException, PermissionBackendException {
     ProjectState projectState = projectCache.get(projectName);
     requireNonNull(projectState, () -> String.format("Failed to load project %s", projectName));
 
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..4977cb3 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -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..fd097d3 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,15 +90,16 @@
         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;
     }
 
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/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 5b7ea3f..771451f 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -34,6 +34,7 @@
 
 package com.google.gerrit.util.cli;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.base.Strings;
@@ -50,8 +51,11 @@
 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;
@@ -85,6 +89,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;
 
@@ -270,6 +357,10 @@
     parser.parseWithPrefix(prefix, bean);
   }
 
+  public void drainOptionQueue() {
+    parser.addOptionsWithMetRequirements();
+  }
+
   private String makeOption(String name) {
     if (!name.startsWith("-")) {
       if (name.length() == 1) {
@@ -336,6 +427,7 @@
 
     PrefixedOption(String prefix, Option o) {
       this.prefix = prefix;
+      checkArgument(o.name().startsWith("-"), "Option name must start with '-': %s", o);
       this.o = o;
     }
 
@@ -404,18 +496,64 @@
     }
   }
 
-  private class MyParser extends org.kohsuke.args4j.CmdLineParser {
+  public class MyParser extends org.kohsuke.args4j.CmdLineParser {
     @SuppressWarnings("rawtypes")
     private List<OptionHandler> optionsList;
 
+    private Map<String, QueuedOption> queuedOptionsByName = new LinkedHashMap<>();
     private HelpOption help;
 
+    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 +568,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(
+                new PrefixedOption(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(
+                new PrefixedOption(prefix, o),
+                Setters.create(f, bean),
+                f.getAnnotation(RequiresOptions.class));
           }
           if (f.isAnnotationPresent(Options.class)) {
             try {
@@ -480,6 +624,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();
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/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 6fbfc30..0000000
--- a/java/com/google/gwtexpui/css/BUILD
+++ /dev/null
@@ -1,7 +0,0 @@
-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/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/org/eclipse/jgit/BUILD b/java/org/eclipse/jgit/BUILD
index 95fef28..af8cde5 100644
--- a/java/org/eclipse/jgit/BUILD
+++ b/java/org/eclipse/jgit/BUILD
@@ -1,43 +1,6 @@
-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"],
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/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 775bfcf0..37b4559 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -57,6 +57,9 @@
 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;
@@ -93,16 +96,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;
@@ -115,11 +119,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;
@@ -191,36 +194,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;
@@ -230,6 +222,8 @@
   @Inject
   private DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners;
 
+  @Inject protected GroupOperations groupOperations;
+
   private AccountIndexedCounter accountIndexedCounter;
   private RegistrationHandle accountIndexEventCounterHandle;
   private RefUpdateCounter refUpdateCounter;
@@ -472,7 +466,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())) {
@@ -522,12 +516,27 @@
 
   @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);
   }
@@ -626,16 +635,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
@@ -712,7 +722,7 @@
 
     accountIndexedCounter.assertNoReindex();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to get stars of another account");
     gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
@@ -775,11 +785,11 @@
     in.reviewer = user2.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).abandon();
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -791,11 +801,11 @@
   public void addReviewerToIgnoredChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
@@ -836,7 +846,7 @@
     String status = "OOO";
     gApi.accounts().id(foo.id.get()).setStatus(status);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
     assertThat(detail._accountId).isEqualTo(foo.id.get());
     assertThat(detail.name).isEqualTo(name);
@@ -858,7 +868,7 @@
     EmailInput input = newEmailInput(secondaryEmail);
     gApi.accounts().id(foo.id.get()).addEmail(input);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
     assertThat(detail.secondaryEmails).isNull();
   }
@@ -880,15 +890,15 @@
     String email = "preferred@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     assertThat(getEmails()).containsExactly(email);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
     gApi.accounts().id(foo.id.hashCode()).addEmail(input);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     assertThat(getEmails()).containsExactly(email, secondaryEmail);
   }
 
@@ -897,7 +907,7 @@
     String email = "preferred2@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
     gApi.accounts().id(foo.id.get()).getEmails();
@@ -929,7 +939,7 @@
       accountIndexedCounter.assertReindexOf(admin);
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).containsAllIn(emails);
   }
 
@@ -964,7 +974,7 @@
   public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
     TestAccount account = accountCreator.create(name("user"));
     EmailInput input = newEmailInput("test@test.com");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.accounts().id(account.username).addEmail(input);
   }
@@ -999,7 +1009,7 @@
       value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     String email = "self@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -1010,7 +1020,7 @@
   public void cannotAddEmailToBeConfirmedToOtherAccountWithoutModifyAccountPermission()
       throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
@@ -1033,14 +1043,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);
   }
 
@@ -1064,13 +1074,13 @@
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .containsAllOf(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()))
@@ -1086,15 +1096,15 @@
     gApi.accounts().id(user.id.get()).addEmail(input);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(getEmails()).contains(email);
 
     // admin can delete email of user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.accounts().id(user.id.get()).deleteEmail(email);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(getEmails()).doesNotContain(email);
 
     // user cannot delete email of admin
@@ -1191,7 +1201,7 @@
 
   @Test
   public void userCannotSetNameOfOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.accounts().id(admin.username).setName("Admin McAdminface");
   }
@@ -1206,7 +1216,7 @@
 
   @Test
   public void fetchUserBranch() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
     String userRefName = RefNames.refsUsers(user.id);
@@ -1263,11 +1273,11 @@
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
     allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), allUsersRepo);
     push.to(RefNames.refsUsers(admin.id)).assertOkStatus();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    push = pushFactory.create(admin.getIdent(), allUsersRepo);
     push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
     accountIndexedCounter.assertReindexOf(admin);
   }
@@ -1278,7 +1288,7 @@
     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.getIdent(), allUsersRepo);
     PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1287,7 +1297,7 @@
     gApi.changes().id(r.getChangeId()).current().submit();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
+    push = pushFactory.create(admin.getIdent(), allUsersRepo);
     r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1310,7 +1320,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1349,7 +1358,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 foo.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1360,7 +1368,7 @@
     accountIndexedCounter.assertNoReindex();
     assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).current().submit();
 
@@ -1382,7 +1390,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1422,7 +1429,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1456,7 +1462,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1496,7 +1501,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1528,7 +1532,6 @@
         ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             allUsersRepo,
             "Add project watch",
@@ -1542,7 +1545,6 @@
         ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
     push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             allUsersRepo,
             "Add invalid project watch",
@@ -1559,7 +1561,7 @@
   @Test
   public void pushAccountConfigToUserBranch() throws Exception {
     TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
-    setApiUser(oooUser);
+    requestScopeOperations.setApiUser(oooUser.getId());
 
     // Must clone as oooUser to ensure the push is allowed.
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
@@ -1572,7 +1574,6 @@
     accountIndexedCounter.clear();
     pushFactory
         .create(
-            db,
             oooUser.getIdent(),
             allUsersRepo,
             "Update account config",
@@ -1598,7 +1599,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1631,7 +1631,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1666,7 +1665,6 @@
 
     pushFactory
         .create(
-            db,
             foo.getIdent(),
             allUsersRepo,
             "Update account config",
@@ -1700,7 +1698,6 @@
 
     pushFactory
         .create(
-            db,
             foo.getIdent(),
             allUsersRepo,
             "Update account config",
@@ -1727,7 +1724,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 allUsersRepo,
                 "Update account config",
@@ -1759,7 +1755,6 @@
 
     pushFactory
         .create(
-            db,
             admin.getIdent(),
             allUsersRepo,
             "Update account config",
@@ -1779,7 +1774,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.getIdent(), allUsersRepo).to(userRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create user branch.");
 
@@ -1796,7 +1791,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.getIdent(), allUsersRepo).to(userRef).assertOkStatus();
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
       assertThat(repo.exactRef(userRef)).isNotNull();
@@ -1812,7 +1807,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.getIdent(), allUsersRepo).to(userRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
 
@@ -1832,7 +1827,7 @@
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     pushFactory
-        .create(db, admin.getIdent(), allUsersRepo)
+        .create(admin.getIdent(), allUsersRepo)
         .to(RefNames.REFS_USERS_DEFAULT)
         .assertOkStatus();
 
@@ -1895,7 +1890,7 @@
     assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
     assertKeys(key);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     exception.expectMessage(id);
     gApi.accounts().self().gpgKey(id).get();
@@ -1932,7 +1927,7 @@
 
     TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("GPG key already associated with another account");
@@ -1947,7 +1942,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);
   }
@@ -2075,12 +2070,12 @@
   @Test
   public void reindexPermissions() throws Exception {
     // admin can reindex any account
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.accounts().id(user.username).index();
     accountIndexedCounter.assertReindexOf(user);
 
     // user can reindex own account
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().index();
     accountIndexedCounter.assertReindexOf(user);
 
@@ -2093,7 +2088,7 @@
   @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");
@@ -2139,7 +2134,7 @@
 
     TestAccount foo2 = accountCreator.create(name + "-2");
     gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    assertThat(gApi.accounts().id(foo2.id.get()).getActive()).isFalse();
 
     assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
   }
@@ -2229,7 +2224,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();
@@ -2273,17 +2270,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,
@@ -2328,7 +2320,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2336,8 +2328,6 @@
                 cfg,
                 retryMetrics,
                 null,
-                null,
-                null,
                 r ->
                     r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
                         .withBlockStrategy(noSleepBlockStrategy)),
@@ -2390,17 +2380,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,
@@ -2461,17 +2446,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,
@@ -2611,7 +2591,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);
@@ -2625,7 +2605,7 @@
               null);
 
       // Create 2 drafts each on both changes for user.
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
@@ -2634,12 +2614,12 @@
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
 
       // Create 1 draft on first change for admin.
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       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.getId());
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
 
@@ -2655,7 +2635,7 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
 
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
     } finally {
       cleanUpDrafts();
@@ -2692,9 +2672,9 @@
   public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
     try {
       PushOneCommit.Result r = createChange();
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       try {
         gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
         assert_().fail("expected AuthException");
@@ -2713,7 +2693,7 @@
       PushOneCommit.Result r1 = createChange();
       PushOneCommit.Result r2 = createChange("refs/for/secret");
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -2745,7 +2725,7 @@
 
   private void cleanUpDrafts() throws Exception {
     for (TestAccount testAccount : accountCreator.getAll()) {
-      setApiUser(testAccount);
+      requestScopeOperations.setApiUser(testAccount.getId());
       for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
         for (CommentInfo c :
             gApi.changes().id(changeInfo.id).drafts().values().stream()
@@ -2873,18 +2853,18 @@
                 u.addExternalId(
                     ExternalId.createWithEmail(name("test"), email, account.getId(), email)));
     accountIndexedCounter.assertReindexOf(account);
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.getId());
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
     Map<String, GpgKeyInfo> gpgKeys =
-        gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
+        gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.of());
     accountIndexedCounter.assertReindexOf(gApi.accounts().self().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 {
@@ -2914,7 +2894,7 @@
         TreeWalk.forPath(
             allUsersRepo.getRepository(),
             AccountProperties.ACCOUNT_CONFIG,
-            getHead(allUsersRepo.getRepository()).getTree())) {
+            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
       assertThat(tw).isNotNull();
       ac.fromText(
           new String(
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 7199f26..edb98d0 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -21,7 +21,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;
@@ -33,6 +32,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 org.eclipse.jgit.lib.Repository;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 7a4a901..29d4aa0 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.getId());
   }
 
   @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.getId());
 
     // 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.getId());
     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.getId());
     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.getId());
+    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.getId());
+    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.getId());
     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.getId());
     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.getId());
 
     // 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.getIdent(), 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/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index b626ab3..691dd94 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.getId());
     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.getId());
     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.getId());
     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 be4b2e1..9cc277f 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;
@@ -60,7 +58,6 @@
 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.ChangeIndexedCounter;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
@@ -68,7 +65,11 @@
 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;
@@ -80,10 +81,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;
@@ -124,14 +127,13 @@
 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.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;
@@ -139,8 +141,11 @@
 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.restapi.change.PostReview;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -154,7 +159,6 @@
 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;
@@ -179,11 +183,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;
@@ -248,158 +255,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 {
-    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).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
@@ -407,32 +267,32 @@
     PushOneCommit.Result rwip = createChange();
     String changeId = rwip.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
 
   @Test
   public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
@@ -453,34 +313,34 @@
     String changeId = rready.getChangeId();
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
@@ -499,8 +359,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);
@@ -634,6 +492,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.getId());
+    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();
@@ -689,12 +578,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWorkInProgressChangeOwner() throws Exception {
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -704,12 +593,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWithWorkInProgressAdmin() throws Exception {
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -720,19 +609,35 @@
   public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
+    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.getId());
     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);
   }
 
@@ -748,7 +653,6 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -833,23 +737,18 @@
 
     // 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().getId());
     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
@@ -915,17 +814,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");
@@ -943,13 +831,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 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 = 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
@@ -966,7 +919,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -988,7 +941,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).rebase();
   }
 
@@ -1009,7 +962,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1045,10 +998,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.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
@@ -1107,7 +1060,7 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
       in.branch = "refs/heads/master";
@@ -1119,7 +1072,7 @@
 
       assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id.get());
 
-      setApiUser(deleteAs);
+      requestScopeOperations.setApiUser(deleteAs.getId());
       gApi.changes().id(changeId).delete();
 
       assertThat(query(changeId)).isEmpty();
@@ -1146,7 +1099,7 @@
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       exception.expect(AuthException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1170,10 +1123,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.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).abandon();
 
     exception.expect(AuthException.class);
@@ -1185,7 +1138,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.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
     gApi.changes().id(changeId).abandon();
@@ -1214,12 +1167,12 @@
 
     try {
       PushOneCommit.Result changeResult =
-          pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+          pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
       String changeId = changeResult.getChangeId();
 
       merge(changeResult);
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       exception.expect(MethodNotAllowedException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1244,6 +1197,23 @@
   }
 
   @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 rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
@@ -1259,7 +1229,6 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1416,7 +1385,7 @@
   @Test
   public void pushCommitOfOtherUser() throws Exception {
     // admin pushes commit of user
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -1446,7 +1415,7 @@
   @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/*");
@@ -1455,7 +1424,7 @@
 
     // admin pushes commit of user
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(user.getIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -1466,7 +1435,7 @@
     assertThat(commit.committer.email).isEqualTo(user.email);
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1486,7 +1455,6 @@
     // admin pushes commit that references 'user' in a footer
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT
@@ -1520,7 +1488,7 @@
   @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/*");
@@ -1531,7 +1499,6 @@
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             repo,
             PushOneCommit.SUBJECT
@@ -1545,7 +1512,7 @@
     result.assertOkStatus();
 
     // check that 'user' cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1554,7 +1521,7 @@
     }
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
     assertThat(change.reviewers.get(REVIEWER)).isNull();
     assertThat(change.reviewers.get(CC)).isNull();
@@ -1564,7 +1531,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/*");
@@ -1573,12 +1540,12 @@
 
     // create change
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1587,7 +1554,7 @@
     }
 
     // try to add user as reviewer
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
     AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
@@ -1602,21 +1569,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);
@@ -1624,7 +1619,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;
@@ -1756,7 +1751,7 @@
             .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);
@@ -1817,7 +1812,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);
@@ -1855,26 +1850,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();
@@ -1884,7 +1859,7 @@
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     // There should be no email notification when adding self
@@ -1921,7 +1896,7 @@
   private void testImplicitlyCcOnNonVotingReviewPgStyle(
       com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(testAccount);
+    requestScopeOperations.setApiUser(testAccount.getId());
     assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
 
     // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
@@ -1932,45 +1907,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.getId());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -1982,19 +1925,18 @@
 
     // 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);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.values()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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()))
+    assertThat(c.reviewers.get(CC).stream().map(ai -> ai._accountId).collect(toList()))
         .containsExactly(user.id.get());
   }
 
@@ -2029,7 +1971,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);
   }
@@ -2090,7 +2032,7 @@
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike());
 
     m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
@@ -2121,12 +2063,9 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).addReviewer(user.getId().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.getId());
 
     sender.clear();
     gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
@@ -2139,9 +2078,9 @@
 
     // 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());
+    c = gApi.changes().id(changeId).get();
+    assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
+    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.getId());
 
     // Remove again, and then try to remove once more to verify 404 is
     // returned.
@@ -2165,7 +2104,7 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend());
 
     Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
@@ -2176,7 +2115,7 @@
     assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     DeleteReviewerInput input = new DeleteReviewerInput();
     if (!notify) {
       input.notify = NotifyHandling.NONE;
@@ -2207,7 +2146,7 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
@@ -2218,14 +2157,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(changeId);
     gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
@@ -2236,13 +2175,13 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
     eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
   }
@@ -2252,14 +2191,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(changeId);
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
@@ -2270,10 +2209,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote("Code-Review");
 
@@ -2305,10 +2244,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
@@ -2334,9 +2273,9 @@
         .preferredEmail(email)
         .fullname("User2")
         .create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
@@ -2344,9 +2283,9 @@
     assertNotifyTo(email, "User2");
 
     // notify unrelated account as CC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
@@ -2354,9 +2293,9 @@
     assertNotifyCc(email, "User2");
 
     // notify unrelated account as BCC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
@@ -2369,7 +2308,7 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("delete vote not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).deleteVote("Code-Review");
@@ -2413,12 +2352,12 @@
 
     // 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.getId());
     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.getId());
     gApi.changes().id(changeId).revision(commit).submit();
 
     // User should still be on the change
@@ -2435,7 +2374,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.");
   }
@@ -2482,6 +2421,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();
@@ -2535,7 +2487,7 @@
     assertThat(
             Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
         .isEqualTo(r.getChangeId());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
   }
 
@@ -2546,7 +2498,7 @@
     in.reviewer = user.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull();
 
     revision(r).review(ReviewInput.recommend());
@@ -2567,7 +2519,7 @@
   public void editTopicWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("edit topic name not permitted");
     gApi.changes().id(r.getChangeId()).topic("mytopic");
@@ -2578,7 +2530,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.getId());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
   }
@@ -2622,7 +2574,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.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("submit not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
@@ -2633,7 +2585,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.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
     assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
   }
@@ -2669,8 +2621,7 @@
     r1.assertOkStatus();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
     r2.assertOkStatus();
 
@@ -2717,16 +2668,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 {
@@ -2757,7 +2701,7 @@
 
   @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     PushOneCommit.Result r1 = createChange();
     gApi.changes()
         .id(r1.getChangeId())
@@ -2767,9 +2711,8 @@
 
     createChange();
 
-    setApiUser(user);
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    requestScopeOperations.setApiUser(user.getId());
+    try (AutoCloseable ignored = disableNoteDb()) {
       assertThat(
               gApi.changes()
                   .query()
@@ -2780,8 +2723,6 @@
                   .withOption(REVIEWED)
                   .get())
           .hasSize(2);
-    } finally {
-      enableDb(ctx);
     }
   }
 
@@ -2834,7 +2775,7 @@
 
   @Test
   public void anonymousRestApi() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     PushOneCommit.Result r = createChange();
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -2853,12 +2794,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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
         .to("refs/for/master")
         .assertOkStatus();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
@@ -2894,7 +2832,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.");
   }
@@ -2914,7 +2852,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);
@@ -2924,7 +2862,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.getIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2944,7 +2882,7 @@
     TestRepository<?> userTestRepo = cloneProject(project, user);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2960,7 +2898,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);
 
@@ -2968,7 +2906,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.getIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -3000,7 +2938,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.getIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3036,7 +2974,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.getIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3072,7 +3010,7 @@
     gApi.changes().id(baseChange).setPrivate(true, "set private");
 
     // Create the destination change on 'master' branch.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     testRepo.reset(initialHead);
     String changeId = createChange().getChangeId();
 
@@ -3210,7 +3148,6 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             "Ignore Verified",
@@ -3257,7 +3194,6 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             "Configure Non-Author-Code-Review",
@@ -3295,10 +3231,10 @@
 
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
     ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
@@ -3312,7 +3248,7 @@
   public void checkLabelsForAutoClosedChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/heads/master");
     result.assertOkStatus();
 
@@ -3406,12 +3342,7 @@
     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();
-    }
+    assertThat(votes).isEmpty();
   }
 
   @Test
@@ -3451,10 +3382,10 @@
 
     String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     PushOneCommit.Result result2 =
-        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
 
     addComment(result1, "comment 1", true, false, null);
     addComment(result2, "comment 2", true, true, null);
@@ -3472,8 +3403,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.getIdent(), testRepo).to("refs/for/master");
     approve(r1.getChangeId());
 
     exception.expect(ResourceConflictException.class);
@@ -3484,8 +3414,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.getIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3504,8 +3433,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.getIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3526,7 +3454,7 @@
         .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.getId());
       String newMessage =
           "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
       gApi.changes().id(r.getChangeId()).setMessage(newMessage);
@@ -3545,7 +3473,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.getId());
     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);
@@ -3583,6 +3511,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();
@@ -3598,12 +3538,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.getIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     // Try to change the commit message
@@ -3633,7 +3573,7 @@
     String subject = "A happy change " + smile;
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
+            .create(admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
             .to("refs/for/master");
     r.assertOkStatus();
     String id = r.getChangeId();
@@ -3791,7 +3731,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
@@ -3811,7 +3751,7 @@
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     // Remove user's permission for 'Label'.
     try (ProjectConfigUpdate u = updateProject(project)) {
       Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
@@ -3823,7 +3763,7 @@
     }
 
     // Verify user's new permitted range.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     change = gApi.changes().id(changeId).get();
     assertPermitted(change, label);
     assertPermitted(change, codeReviewLabel, -1, 0, 1);
@@ -3832,7 +3772,7 @@
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).current().submit();
   }
 
@@ -3861,6 +3801,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));
   }
 
@@ -3882,7 +3825,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();
     }
@@ -3947,7 +3890,6 @@
   public void trackingIds() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT + "\n\n" + "Bug:JRA001",
@@ -4004,13 +3946,13 @@
     in.reviewer = email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     amendChange(r.getChangeId());
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -4031,7 +3973,7 @@
     assertThat(messages).hasSize(1);
     assertThat(messages.get(0).rcpt()).containsExactly(address);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).ignore(false);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse();
   }
@@ -4049,7 +3991,7 @@
   public void cannotIgnoreStarredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().starChange(changeId);
     assertThat(gApi.changes().id(changeId).get().starred).isTrue();
 
@@ -4067,7 +4009,7 @@
   public void cannotStarIgnoredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).ignore(true);
     assertThat(gApi.changes().id(changeId).ignored()).isTrue();
 
@@ -4091,16 +4033,16 @@
     in.reviewer = user.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     sender.clear();
     amendChange(r.getChangeId());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
 
     List<Message> messages = sender.getMessages();
@@ -4112,7 +4054,7 @@
   public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4137,7 +4079,7 @@
   public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(false);
     assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
 
@@ -4162,7 +4104,7 @@
   public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4191,11 +4133,25 @@
 
   @Test
   public void changeDetailsDoesNotRequireIndex() throws Exception {
-    PushOneCommit.Result change = createChange();
-    int number = gApi.changes().id(change.getChangeId()).get()._number;
+    // 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);
 
-    try (AutoCloseable ctx = disableChangeIndex()) {
-      assertThat(gApi.changes().id(project.get(), number).get(ImmutableSet.of()).changeId)
+    PushOneCommit.Result change = createChange();
+    int number = gApi.changes().id(change.getChangeId()).get(options)._number;
+
+    try (AutoCloseable ignored = disableChangeIndex()) {
+      assertThat(gApi.changes().id(project.get(), number).get().changeId)
           .isEqualTo(change.getChangeId());
     }
   }
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..3ae38ca 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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..81519a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -55,7 +55,6 @@
     PushOneCommit.Result gp1 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "grand parent 1",
@@ -66,7 +65,6 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "parent 1",
@@ -80,7 +78,6 @@
     PushOneCommit.Result gp2 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "grand parent 2",
@@ -91,7 +88,6 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 "parent 2",
@@ -101,11 +97,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "merge",
-            ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+            admin.getIdent(), 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..7973241
--- /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.getIdent(), userRepo).to("refs/for/master");
+
+    requestScopeOperations.setApiUser(user.getId());
+    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.getIdent(), 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.getId());
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+  }
+
+  @Test
+  public void cannotSetOtherUsersChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    requestScopeOperations.setApiUser(user.getId());
+    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.getIdent(), userRepo).to("refs/for/master");
+
+    requestScopeOperations.setApiUser(user.getId());
+    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.
+    requestScopeOperations.setApiUser(admin.getId());
+    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);
+    requestScopeOperations.setApiUser(user.getId());
+    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.getIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    merge(result);
+
+    requestScopeOperations.setApiUser(user.getId());
+    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.getIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    gApi.changes().id(changeId).addReviewer(admin.getId().toString());
+    merge(result);
+    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+
+    requestScopeOperations.setApiUser(user.getId());
+    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.getIdent(), 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 b23b2bf..2a295b2 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 {
@@ -444,7 +447,6 @@
   private void rework(String changeId) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -456,11 +458,10 @@
   }
 
   private void trivialRebase(String changeId) throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     testRepo.reset(getRemoteHead());
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             "Other Change",
@@ -487,7 +488,7 @@
 
     testRepo.reset(parent1.getCommit());
 
-    PushOneCommit merge = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit merge = pushFactory.create(admin.getIdent(), testRepo);
     merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -504,7 +505,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.getIdent(), testRepo, changeId);
     merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -528,7 +529,6 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 PushOneCommit.SUBJECT,
@@ -556,20 +556,20 @@
   }
 
   private void vote(TestAccount user, String changeId, String label, int vote) throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     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);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).reviewer(user.getId().toString()).deleteVote(label);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 507205d..9517bea 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -137,7 +137,6 @@
   private PushOneCommit.Result createChange(String dest, String subject) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             subject,
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index 87a566e..491cb3a 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,8 +61,8 @@
   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);
@@ -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..f5ebfea 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -28,6 +28,7 @@
 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.util.concurrent.AtomicLongMap;
@@ -40,6 +41,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 +71,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 +83,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 +94,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 +121,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 +179,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 +196,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 +236,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 +253,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 +267,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 +278,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 +289,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");
   }
 
   @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 +412,7 @@
 
   @Test
   public void createGroupWithoutCapability_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.groups().create(name("newGroup"));
   }
@@ -628,33 +637,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,101 +680,101 @@
 
   @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.getId());
+    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.getId());
+    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);
   }
 
   @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.getId());
+    List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
     assertMembers(listedMembers, user.fullName);
   }
@@ -782,18 +797,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 +834,13 @@
     in.ownerId = adminGroupUuid().get();
     gApi.groups().create(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.groups().id(newGroupName).addMembers(user.username);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
   }
 
@@ -941,8 +959,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
@@ -990,21 +1008,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.getId());
     gApi.groups().id(group.id).index();
 
     // group owner can reindex own group (group is owned by itself)
-    setApiUser(groupOwner);
+    requestScopeOperations.setApiUser(groupOwner.getId());
     gApi.groups().id(group.id).index();
 
     // user cannot reindex any group
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to index group");
     gApi.groups().id(group.id).index();
@@ -1034,7 +1051,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 +1060,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 +1076,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 +1093,7 @@
     repo.reset("groupRef");
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
+            .create(admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .to(groupRefName);
     if (expectedErrorOnUpdate != null) {
       r.assertErrorStatus(expectedErrorOnUpdate);
@@ -1081,21 +1102,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.getIdent(), 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 +1150,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), repo, "Subject", "project.config", config)
+            .create(admin.getIdent(), 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 +1200,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.getIdent(), allUsersRepo).to(groupRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create group branch.");
 
@@ -1283,7 +1303,7 @@
     gApi.groups().create(groupInput).get();
     restartAsSlave();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get();
     ImmutableList<String> groupNames =
         groups.stream().map(group -> group.name).collect(toImmutableList());
@@ -1409,8 +1429,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(
-                db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
+            .create(admin.getIdent(), 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 +1503,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 +1513,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/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 27f737f..752d5f1 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.getId());
     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..4088ebf 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();
 
-    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
@@ -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);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 9fcbaa7..9267bc3 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -31,8 +31,12 @@
 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.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.DescriptionInput;
@@ -51,7 +55,10 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 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.Inject;
+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 +68,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)
@@ -206,14 +222,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 +238,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 +247,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 +353,7 @@
   @Test
   public void nonOwnerCannotSetConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("write refs/meta/config not permitted");
     gApi.projects().name(project.get()).config(input);
@@ -372,7 +388,7 @@
   @Test
   public void setHeadNotAllowed() throws Exception {
     gApi.projects().name(project.get()).branch("test").create(new BranchInput());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted: set HEAD on refs/heads/test");
     gApi.projects().name(project.get()).head("test");
@@ -418,7 +434,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 +443,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 +478,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 +494,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 +509,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 +524,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 +540,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 +557,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 +582,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 +599,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 +619,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);
   }
@@ -677,4 +718,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..e428e89 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.getId());
     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.getId());
     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.getId());
     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/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 057f837..b6969a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -2521,7 +2521,7 @@
       throws Exception {
     testRepo.reset(parentCommit);
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Adjust files of repo", files);
+        pushFactory.create(admin.getIdent(), testRepo, "Adjust files of repo", files);
     PushOneCommit.Result result = push.to("refs/for/master");
     return result.getCommit();
   }
@@ -2545,7 +2545,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.getIdent(), testRepo, "Remove files from repo", files);
     PushOneCommit.Result result = push.rm("refs/for/master");
     return result.getCommit();
   }
@@ -2564,7 +2564,7 @@
 
   private Result createEmptyChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "Test change", ImmutableMap.of());
+        pushFactory.create(admin.getIdent(), 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 b058d5e..7a0fff8 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -41,6 +41,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;
@@ -113,9 +114,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 {
@@ -216,13 +218,13 @@
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     revision(r).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     revision(r).review(ReviewInput.recommend());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).reviewer(user.username).deleteVote("Code-Review");
     Optional<ApprovalInfo> crUser =
         get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
@@ -233,7 +235,7 @@
 
     revision(r).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ReviewInput in = new ReviewInput();
     in.label("Code-Review", 1);
     in.message = "Still LGTM";
@@ -293,7 +295,7 @@
   @Test
   public void voteNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("is restricted");
     gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
@@ -410,7 +412,7 @@
     String subject = "Test change\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
+            admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
     PushOneCommit.Result r2 = push.to("refs/for/master");
 
     // Change 2's parent should be change 1
@@ -467,7 +469,6 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -494,7 +495,6 @@
     String destContent = "some content";
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -506,7 +506,6 @@
     String changeContent = "another content";
     push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -586,7 +585,7 @@
   public void cherryPickToExistingChange() throws Exception {
     PushOneCommit.Result r1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
             .to("refs/for/master");
     String t1 = project.get() + "~master~" + r1.getChangeId();
 
@@ -596,7 +595,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
+            .create(admin.getIdent(), 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 +729,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.getId());
     CherryPickInput input = new CherryPickInput();
     input.message = "it goes to a new branch";
 
@@ -766,13 +765,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().getId());
     ReviewInput in = ReviewInput.approve();
     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().getId());
     CherryPickInput cin = new CherryPickInput();
     cin.message = "this need to go to stable";
     cin.destination = "stable";
@@ -786,16 +785,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 +850,7 @@
     input.base = dstChange.getCommit().name();
     input.message = srcChange.getCommit().getFullMessage();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(
         String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
@@ -877,8 +871,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);
   }
 
@@ -916,11 +910,11 @@
 
   @Test
   public void canRebase() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r2 = push.to("refs/for/master");
     boolean canRebase =
         gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).canRebase();
@@ -928,7 +922,7 @@
     merge(r2);
 
     testRepo.reset(r1.getCommit());
-    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r3 = push.to("refs/for/master");
 
     canRebase = gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).canRebase();
@@ -937,7 +931,7 @@
 
   @Test
   public void setUnsetReviewedFlag() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes().id(r.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
@@ -952,7 +946,7 @@
 
   @Test
   public void setUnsetReviewedFlagByFileApi() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(true);
@@ -971,7 +965,6 @@
 
     PushOneCommit push1 =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -987,7 +980,6 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1107,7 +1099,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.getIdent(), testRepo, SUBJECT, files).to("refs/for/master");
     result.assertOkStatus();
     String changeId = result.getChangeId();
 
@@ -1139,7 +1131,7 @@
   public void setDescriptionNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("edit description not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
@@ -1150,7 +1142,7 @@
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
     grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
   }
@@ -1189,14 +1181,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();
@@ -1420,11 +1405,11 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
     // check if it's blocked to delete a vote on a non-current patch set.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     exception.expect(MethodNotAllowedException.class);
     exception.expectMessage("Cannot access on non-current patch set");
     gApi.changes()
@@ -1443,10 +1428,10 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes()
         .id(r.getChangeId())
         .current()
@@ -1483,11 +1468,11 @@
     assertThat(approval.username).isEqualTo(admin.username);
 
     // Also vote on it with another user
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
     // Patch set 1 has 2 votes on Code-Review
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     votes = gApi.changes().id(changeId).current().votes();
     assertThat(votes.keySet()).containsExactly("Code-Review");
     approvals = votes.get("Code-Review");
@@ -1521,7 +1506,7 @@
       throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
+            admin.getIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
     return push.to("refs/for/master");
   }
 
@@ -1538,7 +1523,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));
@@ -1547,18 +1532,17 @@
 
     PushOneCommit.Result changeAResult =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "change a", parent1FileName, "Content of a")
+            .create(admin.getIdent(), 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.getIdent(), testRepo, "change b", parent2FileName, "Content of b")
             .to("refs/for/" + branchBName);
 
     PushOneCommit pushableMergeCommit =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             "merge",
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index dfd25e2..2f23123 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,7 +67,6 @@
   public void setUp() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             "Provide files which can be used for fixes",
@@ -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.getIdent(), 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.getIdent(), 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);
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 7573a09..8b5975d 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -35,6 +35,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 +45,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;
@@ -55,7 +56,6 @@
 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 +64,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 +77,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 +92,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,7 +111,6 @@
 
   @Before
   public void setUp() throws Exception {
-    db = reviewDbProvider.open();
     changeId = newChange(admin.getIdent());
     ps = getCurrentPatchSet(changeId);
     assertThat(ps).isNotNull();
@@ -120,11 +118,6 @@
     changeId2 = newChange2(admin.getIdent());
   }
 
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void parseEditRevision() throws Exception {
     createArbitraryEditFor(changeId);
@@ -239,15 +232,6 @@
   }
 
   @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);
@@ -294,7 +278,6 @@
     assertThat(edit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -645,11 +628,11 @@
     gApi.changes().id(changeId2).edit().publish(publishInput);
     assertThat(queryEdits()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     createEmptyEditFor(changeId);
     assertThat(queryEdits()).hasSize(1);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertThat(queryEdits()).isEmpty();
   }
 
@@ -690,7 +673,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 +681,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.getIdent(), userTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -753,14 +736,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 +755,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/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 1226661..ede450a 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;
@@ -95,6 +96,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -124,6 +126,7 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+@SkipProjectClone
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
     // TODO(dborowitz): TEST.
@@ -131,6 +134,9 @@
     HTTP
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  private static String NEW_CHANGE_INDICATOR = " [NEW]";
   private LabelType patchSetLock;
 
   @BeforeClass
@@ -163,7 +169,7 @@
 
   @After
   public void resetPublishCommentOnPushOption() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
     prefs.publishCommentsOnPush = false;
     gApi.accounts().id(admin.id.get()).setPreferences(prefs);
@@ -220,6 +226,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);
@@ -317,7 +377,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";
@@ -329,7 +389,7 @@
         .create();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .create(admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
             .to("refs/for/master");
     Change.Id id2 = r2.getChange().getId();
     r2.assertOkStatus();
@@ -337,18 +397,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");
   }
 
@@ -434,7 +493,7 @@
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add(topicOption);
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
 
@@ -467,7 +526,7 @@
     pwi.filter = "*";
     pwi.notifyNewChanges = true;
     projectsToWatch.add(pwi);
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
 
     TestAccount user2 = accountCreator.user2();
@@ -489,12 +548,7 @@
     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.emailAddress);
 
     sender.clear();
     r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
@@ -585,22 +639,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
@@ -666,23 +716,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
@@ -789,7 +835,7 @@
   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.getIdent(), userRepo).to("refs/for/master%wip");
     r.assertOkStatus();
     assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
@@ -806,7 +852,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.getIdent(), userRepo).to("refs/for/master%ready");
     r.assertOkStatus();
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
@@ -851,8 +897,7 @@
     assertThat(edit).isPresent();
     EditInfo editInfo = edit.get();
     r.assertMessage(
-        "Updated Changes:\n  "
-            + canonicalWebUrl.get()
+        canonicalWebUrl.get()
             + "c/"
             + project.get()
             + "/+/"
@@ -888,8 +933,7 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.getIdent(), 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=");
@@ -897,7 +941,6 @@
 
     push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -975,7 +1018,6 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -997,7 +1039,6 @@
 
     push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1016,7 +1057,6 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1191,7 +1231,6 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1207,7 +1246,6 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1240,9 +1278,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);
@@ -1257,7 +1292,6 @@
     String hashtag2 = "tag2";
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1273,9 +1307,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";
@@ -1293,7 +1324,6 @@
     String hashtag4 = "tag4";
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -1308,18 +1338,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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1328,7 +1350,6 @@
 
     push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT
@@ -1340,7 +1361,7 @@
 
     push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertErrorStatus("not Signed-off-by author/committer/uploader in message footer");
   }
@@ -1350,14 +1371,13 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.getIdent(), 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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1375,8 +1395,7 @@
 
     // create a change as admin
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1387,7 +1406,7 @@
     userRepo.reset("change");
     push =
         pushFactory.create(
-            db, user.getIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            user.getIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1405,14 +1424,13 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.getIdent(), 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.
@@ -1422,24 +1440,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.getIdent(), 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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1451,24 +1461,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.getIdent(), 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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1576,7 +1578,6 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT
@@ -1672,8 +1673,7 @@
   @Test
   public void pushCommitWithSameChangeIdAsPredecessorChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1782,7 +1782,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()));
@@ -1800,7 +1800,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));
   }
 
@@ -1808,7 +1808,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.getIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r = push1.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1850,7 +1850,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();
@@ -1889,7 +1889,6 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -2256,16 +2255,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();
@@ -2312,6 +2301,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.getIdent()).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.getIdent(), 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;
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 e222a4f..0c43d8c 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -7,6 +7,7 @@
     deps = [
         ":push_for_review",
         ":submodule_util",
+        "//lib/commons:lang",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index d80faa8..652a836 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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/GitOverHttpServletIT.java b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
deleted file mode 100644
index 42e046a..0000000
--- a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.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.git;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.server.AuditEvent;
-import java.util.Collections;
-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 GitOverHttpServletIT extends AbstractPushForReview {
-
-  @Before
-  public void beforeEach() throws Exception {
-    CredentialsProvider.setDefault(
-        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
-    selectProtocol(AbstractPushForReview.Protocol.HTTP);
-    auditService.clearEvents();
-  }
-
-  @Test
-  public void receivePackAuditEventLog() throws Exception {
-    testRepo
-        .git()
-        .push()
-        .setRemote("origin")
-        .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);
-
-    AuditEvent e = auditService.auditEvents.get(1);
-    assertThat(e.who.getAccountId()).isEqualTo(admin.id);
-    assertThat(e.what).endsWith("/git-receive-pack");
-    assertThat(e.params).isEmpty();
-  }
-
-  @Test
-  public void uploadPackAuditEventLog() throws Exception {
-    testRepo.git().fetch().call();
-
-    assertThat(auditService.auditEvents.size()).isEqualTo(1);
-
-    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");
-  }
-}
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..6309e79 100644
--- a/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -14,15 +14,82 @@
 
 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));
     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)) {
+      new TestRepository<>(repo).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..eed506c 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.getIdent(), 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..dd0a5cb 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)) {
@@ -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.getId());
     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.getId());
     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..ff4a423 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getId());
     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.getId());
     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.getId());
     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.getId());
+    gApi.changes().id(cd3.getId().get()).edit().create();
 
     // User's edit is visible.
-    setApiUser(user);
-    gApi.changes().id(changeId).edit().create();
+    requestScopeOperations.setApiUser(user.getId());
+    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.getId());
+    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.getId());
+    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.getId());
+    gApi.changes().id(cd3.getId().get()).edit().create();
+    requestScopeOperations.setApiUser(user.getId());
 
     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.getId());
+    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.getId());
+    assertUploadPackRefs(
+        "HEAD",
+        "refs/meta/config",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
   }
 
   @Test
@@ -396,32 +407,32 @@
     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.getId());
 
-    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
@@ -430,44 +441,36 @@
     try (Repository repo = repoManager.openRepository(project)) {
       TestRepository<?> 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.getId()), 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
@@ -500,52 +503,42 @@
   }
 
   @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 +554,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 +568,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 +601,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.getId());
     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 +631,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.getId());
+    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 +646,6 @@
   }
 
   @Test
-  @GerritConfig(name = "noteDb.groups.write", value = "true")
   public void hideMetadata() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     // create change
@@ -664,7 +654,7 @@
     allUsersRepo.reset("userRef");
     PushOneCommit.Result mr =
         pushFactory
-            .create(db, admin.getIdent(), allUsersRepo)
+            .create(admin.getIdent(), allUsersRepo)
             .to("refs/for/" + RefNames.REFS_USERS_SELF);
     mr.assertOkStatus();
 
@@ -684,15 +674,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 +693,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 +733,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 +762,7 @@
     ReceiveCommitsAdvertiseRefsHook hook =
         new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
     try (Repository repo = repoManager.openRepository(project)) {
-      return hook.advertiseRefs(repo.getAllRefs());
+      return hook.advertiseRefs(getAllRefs(repo));
     }
   }
 
@@ -805,18 +793,8 @@
     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/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 700b18b..f583274 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -160,7 +160,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);
@@ -184,7 +184,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);
@@ -194,7 +194,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();
       }
     }
   }
@@ -218,7 +218,6 @@
 
     PushOneCommit push =
         pushFactory.create(
-            db,
             admin.getIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
@@ -231,7 +230,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");
@@ -261,9 +260,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);
@@ -302,14 +301,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());
@@ -317,8 +316,8 @@
   }
 
   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 {
@@ -352,8 +351,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.getIdent(), testRepo, subject, fileName, content);
     return push.to(ref);
   }
 
@@ -361,7 +359,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.getIdent(), 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..e422c95 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);
@@ -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";
@@ -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.getIdent(), repo2, "Change 2", "b.txt", "other content");
       PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
       approve(pushResult2.getChangeId());
 
@@ -659,71 +604,64 @@
 
   @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)))) {
+  private ObjectId directUpdateRef(Project.NameKey project, String ref) throws Exception {
+    try (Repository serverRepo = repoManager.openRepository(project)) {
       return new TestRepository<>(serverRepo).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 +674,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/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 40abd6b..84887da 100644
--- a/javatests/com/google/gerrit/acceptance/rest/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -2,23 +2,9 @@
 
 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 4a2c81b..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
+++ /dev/null
@@ -1,110 +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"));
-
-  /**
-   * 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..0a5510a 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index f3fe68a..0bdb4e4 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);
   }
@@ -142,7 +141,7 @@
                         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);
   }
@@ -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);
@@ -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 b74a0d7..8e0aa01 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -33,6 +33,7 @@
 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.extensions.api.config.ConsistencyCheckInfo;
@@ -90,6 +91,7 @@
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void getExternalIds() throws Exception {
@@ -109,7 +111,7 @@
 
   @Test
   public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts().id(admin.id.get()).getExternalIds();
@@ -135,7 +137,7 @@
 
   @Test
   public void deleteExternalIds() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
     List<String> toDelete = new ArrayList<>();
@@ -162,7 +164,7 @@
   @Test
   public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts()
@@ -173,7 +175,7 @@
   @Test
   public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
     gApi.accounts()
@@ -199,7 +201,7 @@
 
     assertThat(toDelete).hasSize(1);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     RestResponse response =
         userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete);
     response.assertNoContent();
@@ -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();
 
@@ -980,12 +982,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/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 65c95f8..b001d14 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;
@@ -116,7 +113,7 @@
     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);
@@ -191,7 +188,6 @@
 
   @Test
   public void voteOnBehalfOfWithCommentWritingJson() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     testVoteOnBehalfOfWithComment();
   }
 
@@ -218,7 +214,7 @@
     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);
@@ -226,7 +222,6 @@
 
   @Test
   public void voteOnBehalfOfWithRobotComment() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
@@ -257,7 +252,7 @@
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
@@ -265,7 +260,7 @@
     di.message = "message";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = new ReviewInput();
     in.onBehalfOf = user.id.toString();
     in.label("Code-Review", 1);
@@ -313,7 +308,7 @@
   @Test
   public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowCodeReviewOnBehalfOf();
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
 
     PushOneCommit.Result r = createChange();
@@ -340,9 +335,9 @@
     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());
+        approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
     assertThat(submitter.getAccountId()).isEqualTo(admin2.id);
     assertThat(submitter.getRealAccountId()).isEqualTo(admin.id);
   }
@@ -394,7 +389,7 @@
   @Test
   public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowSubmitOnBehalfOf();
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
 
     PushOneCommit.Result r = createChange();
@@ -455,14 +450,14 @@
     allowRunAs();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
 
     // Things that aren't allowed with on_behalf_of:
     //  - no labels.
@@ -484,7 +479,7 @@
     assertThat(c.author._accountId).isEqualTo(user.id.get());
     assertThat(c.message).isEqualTo(di.message);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
   }
 
@@ -520,7 +515,7 @@
     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
@@ -536,7 +531,7 @@
     in.message = "Message on behalf of";
     in.label("Code-Review", 1);
 
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index bc84593..3a68323 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);
 
@@ -57,8 +63,8 @@
 
   @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<>();
 
@@ -91,7 +97,7 @@
 
   @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<>();
 
@@ -150,7 +156,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getId());
     gApi.accounts().self().deleteWatchedProjects(d);
   }
 
@@ -176,7 +182,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
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..dd6e1a5
--- /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.getId(),
+            u ->
+                u.addExternalId(
+                    ExternalId.createWithEmail(name("test"), email, admin.getId(), email)));
+
+    requestScopeOperations.setApiUser(admin.getId());
+    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..6addf51
--- /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..02c44ef
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -0,0 +1,112 @@
+// 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"));
+
+  /**
+   * 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..b37bb01
--- /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..8a8ea9b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -0,0 +1,199 @@
+// 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 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.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"));
+
+  /** 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..c838cf9
--- /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>.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/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..68622e2 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;
@@ -116,14 +117,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 +139,6 @@
   }
 
   @After
-  public void cleanup() {
-    db.close();
-  }
-
-  @After
   public void removeOnSubmitValidator() {
     if (onSubmitValidatorHandle != null) {
       onSubmitValidatorHandle.remove();
@@ -155,12 +150,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 +309,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.getIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -329,7 +323,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,7 +333,7 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -348,14 +342,14 @@
 
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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 +359,17 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), 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());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     submit(result.getChangeId());
   }
 
@@ -385,8 +379,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 +415,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 =
@@ -584,13 +580,12 @@
     }
 
     PushOneCommit push1 =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.getIdent(), 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.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result c2 = push2.to("refs/heads/topic");
     c2.assertOkStatus();
 
@@ -614,11 +609,10 @@
     //
     RevCommit master = getRemoteHead(project, "master");
     PushOneCommit stableTip =
-        pushFactory.create(
-            db, admin.getIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
+        pushFactory.create(admin.getIdent(), 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.getIdent(), 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 +639,11 @@
     // push directly to stable to S1
     PushOneCommit.Result s1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "new commit into stable", "stable1.txt", "")
+            .create(admin.getIdent(), 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.getIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
         .to("refs/heads/stable");
 
     testRepo.reset(initial);
@@ -657,12 +651,12 @@
     // move the master ahead
     PushOneCommit.Result m =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, "Move master ahead", "master.txt", "")
+            .create(admin.getIdent(), 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.getIdent(), 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 +736,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 +760,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 +787,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 +826,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.getIdent(), testRepo, "initial commit", "a.txt", "a");
     PushOneCommit.Result change = push.to("refs/heads/stable");
 
     RevCommit stable = getRemoteHead(project, "stable");
@@ -902,8 +890,6 @@
 
   @Test
   public void retrySubmitSingleChangeOnLockFailure() throws Exception {
-    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
-
     PushOneCommit.Result change = createChange();
     String id = change.getChangeId();
     approve(id);
@@ -929,13 +915,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 +949,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 +1062,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 +1077,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.getIdent(), 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 +1088,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,7 +1099,7 @@
   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() {
@@ -1274,9 +1259,9 @@
 
   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());
@@ -1284,9 +1269,9 @@
 
   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 +1285,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 +1338,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.getIdent(), 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..21d07a7 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.getIdent(), 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.getIdent(), 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..36595ce 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.getId());
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
@@ -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.getIdent(), 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..bd31c5f 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.getId());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     String changeWithSameTopic = createChangeWithTopic().getChangeId();
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag3 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getId());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
     assertThat(etag2).isEqualTo(etag1);
   }
@@ -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..0b39f0a 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() {
@@ -79,7 +82,6 @@
 
   @Test
   public void getPastAssignees() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
     setAssignee(r, user.email);
     setAssignee(r, admin.email);
@@ -92,14 +94,7 @@
 
   @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();
@@ -135,9 +130,28 @@
   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);
+    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.getId().get()).setActive(false);
+    setAssignee(r, user.getId().toString());
+    assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get());
   }
 
   @Test
@@ -153,7 +167,7 @@
   @Test
   public void setAssigneeNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted");
     setAssignee(r, user.email);
@@ -163,7 +177,7 @@
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
   }
 
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..3873c9d 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
@@ -139,7 +143,7 @@
   @Test
   public void deleteCannotBeAppliedWithoutAdministrateServerCapability() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     try {
       deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -153,7 +157,7 @@
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
   }
 
@@ -209,22 +213,22 @@
   private int createOneChangeWithMultipleChangeMessagesInHistory() throws Exception {
     // Creates the following commit history on the meta branch of the test change.
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // Commit 1: create a change.
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     // Commit 2: post a review with message "message 1".
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     addOneReview(changeId, "message 1");
     // Commit 3: amend a new patch set.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     amendChange(changeId);
     // Commit 4: post a review with message "message 2".
     addOneReview(changeId, "message 2");
     // Commit 5: amend a new patch set.
     amendChange(changeId);
     // Commit 6: approve the change.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     // commit 7: submit the change.
     gApi.changes().id(changeId).current().submit();
@@ -251,10 +255,7 @@
     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);
@@ -271,11 +272,9 @@
     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, deletedMessageIndex, deletedBy, reason);
   }
 
   private void assertMessagesAfterDeletion(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index ac0d0aa..993c10e 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.getId());
     user2 = accountCreator.user2();
   }
 
@@ -61,22 +66,22 @@
 
   @Test
   public void testChangeOwner_OwnerACLGrantedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     grantApproveToChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     approve(user, createMyChange(childRepo));
   }
 
   @Test
   public void testChangeOwner_BlockedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     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.getIdent(), 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..983ad21 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,7 +73,6 @@
 
   @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());
 
@@ -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.getId());
       revision(r).review(reviewInput);
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
 
       sender.clear();
 
@@ -221,7 +216,6 @@
 
   @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++) {
@@ -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..dec839c 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,12 +64,16 @@
 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;
@@ -129,31 +135,18 @@
     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.body()).contains(admin.fullName + " has uploaded this change for review.");
   }
 
   @Test
@@ -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();
@@ -209,7 +193,7 @@
     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);
     result = addReviewer(changeId, in);
@@ -217,19 +201,10 @@
     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);
@@ -238,11 +213,6 @@
     for (int i = 0; i < 3; i++) {
       expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
     }
-    if (!notesMigration.readChanges()) {
-      for (int i = 0; i < 3; i++) {
-        expectedAddresses.add(users.get(i).emailAddress);
-      }
-    }
     expectedAddresses.add(reviewer.emailAddress);
     assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
   }
@@ -256,13 +226,8 @@
     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
@@ -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
@@ -444,14 +389,8 @@
     // 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();
@@ -479,8 +418,8 @@
       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)
@@ -544,23 +483,12 @@
     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();
@@ -573,7 +501,7 @@
 
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // NoteDb adds reviewer to a change on every review.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
@@ -622,8 +550,8 @@
         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");
+    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);
 
@@ -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);
@@ -734,9 +659,9 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
@@ -753,7 +678,7 @@
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email);
     assertThatUserIsOnlyReviewer(r.getChangeId());
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
   }
@@ -764,7 +689,7 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email);
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
@@ -779,7 +704,7 @@
     input.reviewer = user.email;
     input.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
@@ -787,7 +712,6 @@
 
   @Test
   public void addExistingReviewerShortCircuits() throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
@@ -804,7 +728,6 @@
 
   @Test
   public void addExistingCcShortCircuits() 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/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index baf56de..b6547f0 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.getId());
     fetchRefsMetaConfig();
   }
 
@@ -96,13 +100,13 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void onlyAdminMayUpdateProjectParent() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ProjectInput parent = new ProjectInput();
     parent.name = name("parent");
     parent.permissionsOnly = true;
     gApi.projects().create(parent);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     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.getId());
     // 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.getIdent(), 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.getIdent(), 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..6fd3bab 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();
@@ -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 9218336..9a6b823 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;
@@ -48,9 +48,11 @@
 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.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -65,6 +67,8 @@
 import org.junit.Test;
 
 public class CreateChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @BeforeClass
   public static void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -150,11 +154,11 @@
 
   @Test
   public void notificationsOnChangeCreation() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(project.get());
 
     // check that watcher is notified
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
 
     List<Message> messages = sender.getMessages();
@@ -265,19 +269,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)) {
@@ -455,6 +447,30 @@
     assertThat(revInfo.commit.message).isEqualTo(input.message + "\n");
   }
 
+  @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 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");
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
@@ -468,12 +484,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);
@@ -505,7 +529,7 @@
       assertThat(o.signedOffBy).isNull();
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
   }
 
   private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
@@ -539,7 +563,7 @@
     // create a initial commit in master
     Result initialCommit =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .create(user.getIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
             .to("refs/heads/master");
     initialCommit.assertOkStatus();
 
@@ -550,13 +574,13 @@
     // create a commit in branchA
     Result changeA =
         pushFactory
-            .create(db, user.getIdent(), testRepo, "change A", fileA, "A content")
+            .create(user.getIdent(), 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.getIdent(), 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 7e5ebdb..1c1c455 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.getId());
     recommend(r.getChangeId());
 
     sender.clear();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 864f08d..2b0ba4e 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.getId());
     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.getId());
     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..cf5136b 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");
+    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.getIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
     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.getId());
     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.getId());
     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.getId());
     gApi.groups().id(group).removeMembers("user");
 
     // User can no longer see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).isEmpty();
 
     // Reindex the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).index();
 
     // Other user can still see the change and it is still mergeable
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     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..ae6c14c 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.getIdent(), testRepo, subject, fileName, content, baseChangeId);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     return r;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 206cc68..7d97967 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
@@ -180,7 +186,7 @@
         r.getChange().change().getDest().get(),
         Permission.ABANDON,
         systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("move not permitted");
     move(r.getChangeId(), newBranch.get());
@@ -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.getIdent(), 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..82fc426 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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..cde75a0 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 {
@@ -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..ccb684c 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;
 
@@ -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/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index c1dda00..147abe1 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() {
@@ -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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getId());
 
     // 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.getIdent(), 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.getIdent(), 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.getId());
 
     // 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.getIdent(), 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.getId());
+
+    // 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..d9a26aa 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() {
@@ -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/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index e9ac07a..b4455f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -307,7 +307,7 @@
   private void assertChangeSetMergeable(ChangeData change, boolean expected)
       throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException,
           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 +333,7 @@
       List<RevCommit> parents,
       String ref)
       throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), repo, subject, fileName, content);
 
     if (!parents.isEmpty()) {
       push.setParents(parents);
@@ -351,7 +351,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 +366,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..13321d0 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,14 @@
 
   @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 +110,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,
@@ -129,16 +135,16 @@
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     reviewers = suggestReviewers(changeId, user2.fullName, 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
 
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
@@ -149,9 +155,9 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     block("refs/*", "read", ANONYMOUS_USERS);
-    allow("refs/*", "read", group1.getGroupUUID());
+    allow("refs/*", "read", group1);
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).isEmpty();
   }
@@ -162,12 +168,13 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user1); // Clear cached group info.
-    allowGlobalCapabilities(group1.getGroupUUID(), GlobalCapability.VIEW_ALL_ACCOUNTS);
+    // Clear cached group info.
+    requestScopeOperations.setApiUser(user1.getId());
+    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);
@@ -243,17 +250,11 @@
   }
 
   @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,42 +266,89 @@
     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.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId3 = createChangeFromApi();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
@@ -318,7 +366,7 @@
   @Test
   public void defaultReviewerSuggestionOnFirstChange() throws Exception {
     TestAccount user1 = user("customuser1", "User1");
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChange().getChangeId(), "", 4);
     assertThat(reviewers).isEmpty();
   }
@@ -337,23 +385,23 @@
     TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
 
     // Create a change as userWhoOwns and add some reviews
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
     // Create a comment as a different user
-    setApiUser(userWhoComments);
+    requestScopeOperations.setApiUser(userWhoComments.getId());
     ReviewInput ri = new ReviewInput();
     ri.message = "Test";
     gApi.changes().id(changeId1).revision(1).review(ri);
@@ -361,7 +409,7 @@
     // Create a change as a new user to assert that we receive the correct
     // ranking
 
-    setApiUser(userWhoLooksForSuggestions);
+    requestScopeOperations.setApiUser(userWhoLooksForSuggestions.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Pri", 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
         .containsExactly(
@@ -372,7 +420,7 @@
   @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,25 +428,25 @@
     TestAccount reviewer1 = user("customuser2", fullName);
     TestAccount reviewer2 = user("customuser3", fullName);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId2 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId3 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId3);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Prim", 4);
 
     // Assert that reviewer1 is on top, even though reviewer2 has more reviews
@@ -422,7 +470,7 @@
         suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
 
     gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    assertThat(gApi.accounts().id(foo2.id.get()).getActive()).isFalse();
     assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
   }
 
@@ -444,7 +492,7 @@
     String secondaryEmail = "foo.secondary@example.com";
     createAccountWithSecondaryEmail("foo", secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<SuggestedReviewerInfo> reviewers =
         suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
     assertThat(reviewers).isEmpty();
@@ -464,7 +512,7 @@
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
         .containsExactly(secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
     assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
@@ -490,21 +538,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,10 +572,10 @@
     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)
@@ -541,7 +588,7 @@
         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..259c4d2 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,18 +34,21 @@
 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);
+    requestScopeOperations.setApiUser(admin.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
     prefs.workInProgressByDefault = false;
     gApi.accounts().id(admin.id.get()).setPreferences(prefs);
@@ -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.getIdent(), 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..3f76a27 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;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 874e04e..14521cc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -70,7 +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")
 
@@ -128,10 +127,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
@@ -197,13 +193,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/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 2722648..d1a80c7 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.getId());
     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.getId());
     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.getId());
 
     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.getId());
     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.getId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getId());
     pApi().branch("refs/heads/master").get();
   }
 
@@ -335,7 +325,7 @@
     accessInput.add.put(REFS_ALL, accessSectionInfo);
     pApi().access(accessInput);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -355,7 +345,7 @@
     AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
     accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     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.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -501,7 +491,7 @@
 
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -571,7 +561,7 @@
     config = cfg.toText();
     PushOneCommit push =
         pushFactory.create(
-            db, admin.getIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
+            admin.getIdent(), 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 64bd5c3..7af7d04 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -8,6 +8,7 @@
         ":project",
         ":push_tag_util",
         ":refassert",
+        "//lib/commons:lang",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index df89686..8943125 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;
@@ -34,10 +34,13 @@
 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.junit.Before;
 import org.junit.Test;
 
 public class CreateBranchIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private Branch.NameKey testBranch;
 
   @Before
@@ -60,7 +63,7 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -78,7 +81,7 @@
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateSucceeds(testBranch);
   }
 
@@ -92,7 +95,7 @@
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden() throws Exception {
     grantOwner();
     blockCreateReference();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -116,7 +119,6 @@
   }
 
   @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..7d36e71 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");
@@ -321,7 +325,7 @@
   public void createProjectWithCapability() throws Exception {
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       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.getId());
     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.getId());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index b426a37..8ca6b75 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -20,8 +20,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;
@@ -31,23 +32,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.getId());
     assertDeleteForbidden(testBranch);
   }
 
@@ -59,7 +63,7 @@
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -73,21 +77,21 @@
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -138,7 +142,6 @@
   }
 
   @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 c1bd8f1..a85004e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.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;
@@ -32,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 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;
@@ -44,6 +46,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);
@@ -70,14 +74,14 @@
 
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     assertBranches(BRANCHES);
   }
 
@@ -85,14 +89,14 @@
   public void deleteMultiBranchesWithoutPermissionForbidden() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       project().deleteBranches(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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..27de4b1 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.getId());
     assertDeleteForbidden();
   }
 
@@ -52,7 +56,7 @@
   @Test
   public void deleteTagByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds();
   }
 
@@ -66,21 +70,21 @@
   public void deleteTagByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden();
   }
 
   @Test
   public void deleteTagByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds();
   }
 
   @Test
   public void deleteTagByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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..6d8689a 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.getId());
     try {
       project().deleteTags(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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..1963455 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.getIdent(), 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.getIdent(), 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..7f2b35e 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.getId());
     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.getId());
     // 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.getId());
     // 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 7189a03..f2c369e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -27,6 +27,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;
@@ -35,9 +37,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;
@@ -54,19 +55,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.getId());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -74,7 +77,7 @@
       u.save();
     }
 
-    assertThatNameList(filter(gApi.projects().list().get())).doesNotContain(project);
+    assertThatNameList(gApi.projects().list().get()).doesNotContain(project);
   }
 
   @Test
@@ -103,16 +106,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
@@ -186,91 +188,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());
@@ -283,36 +281,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();
@@ -323,8 +299,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;
@@ -336,18 +311,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())
@@ -355,8 +327,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));
@@ -370,16 +341,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..9088afa 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.getIdent(), testRepo, "Create Project Level Config", configName, cfg.toText());
     push.to(RefNames.REFS_CONFIG);
 
     ProjectState state = projectCache.get(project);
@@ -73,7 +72,6 @@
 
     pushFactory
         .create(
-            db,
             admin.getIdent(),
             testRepo,
             "Create Project Level Config",
@@ -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,7 +91,6 @@
 
     pushFactory
         .create(
-            db,
             admin.getIdent(),
             childTestRepo,
             "Create Project Level Config",
@@ -128,7 +125,6 @@
 
     pushFactory
         .create(
-            db,
             admin.getIdent(),
             testRepo,
             "Create Project Level Config",
@@ -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,7 +150,6 @@
 
     pushFactory
         .create(
-            db,
             admin.getIdent(),
             childTestRepo,
             "Create Project Level Config",
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index d4edc0d..c0f3732 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.junit.Test;
@@ -56,6 +58,8 @@
           + "=XFeC\n"
           + "-----END PGP SIGNATURE-----";
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -71,7 +75,7 @@
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
     blockRead("refs/*");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tags().get();
   }
@@ -126,7 +130,7 @@
   public void listTagsOfNonVisibleBranch() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push1 = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
     TagInput tag1 = new TagInput();
@@ -137,7 +141,7 @@
     assertThat(result.revision).isEqualTo(tag1.revision);
 
     pushTo("refs/heads/hidden");
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push2 = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r2 = push2.to("refs/heads/hidden");
     r2.assertOkStatus();
 
@@ -166,7 +170,7 @@
   public void lightweightTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
@@ -187,7 +191,7 @@
     assertThat(result.canDelete).isTrue();
     assertThat(result.created).isEqualTo(timestamp(r));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     result = tag(input.ref).get();
     assertThat(result.canDelete).isNull();
 
@@ -198,7 +202,7 @@
   public void annotatedTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
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..cc72e8a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
@@ -0,0 +1,10 @@
+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..d99fa72
--- /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()).isSameAs(self.get());
+
+    result = resolveAsResult("me");
+    assertThat(result.asIdSet()).containsExactly(admin.id);
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameAs(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()).isSameAs(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 e9d2611..2a7cadc 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;
@@ -77,20 +76,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.getId());
   }
 
   @Test
@@ -146,7 +142,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -170,7 +166,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -202,7 +198,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -276,7 +272,7 @@
   public void listComments() throws Exception {
     String file = "file";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, "contents");
+        pushFactory.create(admin.getIdent(), testRepo, "first subject", file, "contents");
     PushOneCommit.Result r = push.to("refs/for/master");
     String changeId = r.getChangeId();
     String revId = r.getCommit().getName();
@@ -385,7 +381,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(db, admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -428,7 +424,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
             .to("refs/for/master");
     changeId = r2.getChangeId();
     revId = r2.getCommit().getName();
@@ -443,11 +439,10 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
@@ -457,13 +452,13 @@
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts();
     assertThat(actual.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> comments = actual.get(FILE_NAME);
@@ -490,8 +485,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(
-                db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
             .to("refs/for/master");
 
     addComment(r1, "nit: trailing whitespace");
@@ -534,13 +528,12 @@
   public void publishCommentsAllRevisions() throws Exception {
     PushOneCommit.Result r1 =
         pushFactory
-            .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
+            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
             .to("refs/for/master");
 
     PushOneCommit.Result r2 =
         pushFactory
             .create(
-                db,
                 admin.getIdent(),
                 testRepo,
                 SUBJECT,
@@ -581,11 +574,11 @@
         other.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     // 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.getId());
 
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
@@ -719,7 +712,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();
@@ -748,16 +741,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);
     }
   }
 
@@ -775,7 +768,7 @@
     String uuid = commentsMap.get(targetComment.path).get(0).id;
     DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
   }
@@ -846,12 +839,9 @@
     // PS4 has comments [c7, c8].
     assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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;
@@ -870,9 +860,7 @@
       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);
@@ -919,12 +907,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.getId());
     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);
@@ -938,21 +923,18 @@
     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..c80cd21 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,32 +26,27 @@
 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;
@@ -88,8 +81,6 @@
 
   @Inject private ChangeNoteUtil noteUtil;
 
-  @Inject @AnonymousCowardName private String anonymousCowardName;
-
   @Inject private Sequences sequences;
 
   private RevCommit tip;
@@ -97,11 +88,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 =
@@ -134,38 +120,6 @@
     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"));
-  }
-
   // No test for ref existing but object missing; InMemoryRepository won't let
   // us do such a thing.
 
@@ -201,7 +155,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 +165,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 +179,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 +195,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 +226,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);
-
     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 +258,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 +274,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 =
@@ -386,7 +323,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 +340,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 +360,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 +379,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 +401,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 +422,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 +452,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 +484,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 +493,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 +532,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 +540,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 +572,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 +589,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 +620,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 +657,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 +667,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 +701,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 +733,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 {
@@ -812,16 +749,16 @@
     ChangeInserter ins;
     try (BatchUpdate bu = newUpdate(owner.getId())) {
       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 +773,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 +802,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 +817,7 @@
             + "Subject: "
             + subject
             + "\n");
-    indexer.index(db, c.getProject(), c.getId());
+    indexer.index(c.getProject(), c.getId());
 
     return ps;
   }
@@ -897,9 +829,6 @@
   }
 
   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);
@@ -921,7 +850,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)) {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index dd47c01..183cfe2 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -22,17 +22,27 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.truth.Correspondence;
 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;
@@ -45,7 +55,10 @@
 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.getIdent(), 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,84 @@
       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 new Correspondence<RelatedChangeAndCommitInfo, String>() {
+      @Override
+      public boolean compare(RelatedChangeAndCommitInfo relatedChangeAndCommitInfo, String status) {
+        return Objects.equals(relatedChangeAndCommitInfo.status, status);
+      }
+
+      @Override
+      public String toString() {
+        return "has status";
+      }
+    };
   }
 
   private RevCommit parseBody(RevCommit c) throws Exception {
@@ -612,15 +690,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();
+              PatchSet ps = psUtil.get(ctx.getNotes(), psId);
+              psUtil.setGroups(ctx.getUpdate(psId), ps, ImmutableList.of());
               return true;
             }
           });
@@ -630,12 +707,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 +727,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/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..79cb097 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -18,17 +18,20 @@
 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();
@@ -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.getIdent(), testRepo, "first subject", file, contents);
     PushOneCommit.Result r = push.to("refs/for/master");
     String changeId = r.getChangeId();
 
     // Review it
-    setApiUser(reviewer);
+    requestScopeOperations.setApiUser(reviewer.getId());
     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..2845180 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.getId());
     AbandonInput in = new AbandonInput();
     if (notify != null) {
       in.notify = notify;
@@ -255,31 +272,7 @@
    * AddReviewerSender tests.
    */
 
-  private void addReviewerToReviewableChangeInReviewDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    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();
+  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);
@@ -290,20 +283,20 @@
         .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);
@@ -314,20 +307,20 @@
         .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");
@@ -339,20 +332,20 @@
         .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");
@@ -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();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -435,25 +411,35 @@
     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());
+    StagedChange sc = stageReviewableWipChange();
+    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
+    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)
+        .cc(sc.reviewer)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .noOneElse();
   }
 
-  private void addReviewerToWipChangeInNoteDbNotifyAll(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  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);
@@ -464,44 +450,20 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToWipChangeInNoteDbNotifyAllSingly() throws Exception {
-    addReviewerToWipChangeInNoteDbNotifyAll(singly());
+  public void addReviewerToWipChangeNotifyAllSingly() throws Exception {
+    addReviewerToWipChangeNotifyAll(singly());
   }
 
   @Test
-  public void addReviewerToWipChangeInNoteDbNotifyAllBatch() throws Exception {
-    addReviewerToWipChangeInNoteDbNotifyAll(batch());
+  public void addReviewerToWipChangeNotifyAllBatch() throws Exception {
+    addReviewerToWipChangeNotifyAll(batch());
   }
 
-  private void addReviewerToWipChangeInReviewDbNotifyAll(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isFalse();
-    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();
+  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, OWNER_REVIEWERS);
@@ -512,62 +474,56 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersSingly() throws Exception {
-    addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(singly());
+  public void addReviewerToReviewableChangeNotifyOwnerReviewersSingly() throws Exception {
+    addReviewerToReviewableChangeNotifyOwnerReviewers(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersBatch() throws Exception {
-    addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(batch());
+  public void addReviewerToReviewableChangeNotifyOwnerReviewersBatch() throws Exception {
+    addReviewerToReviewableChangeNotifyOwnerReviewers(batch());
   }
 
-  private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(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, OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerSingly()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(singly());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwnerSingly() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwner(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerBatch()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(batch());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwnerBatch() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyOwner(batch());
   }
 
-  private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(Adder adder)
+  private void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(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();
+    assertThat(sender).didNotSend();
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneSingly()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(singly());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNoneSingly() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(singly());
   }
 
   @Test
-  public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneBatch()
-      throws Exception {
-    addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(batch());
+  public void addReviewerToReviewableChangeByOwnerCcingSelfNotifyNoneBatch() throws Exception {
+    addReviewerToReviewableChangeByOwnerCcingSelfNotifyNone(batch());
   }
 
-  private void addNonUserReviewerByEmailInNoteDb(Adder adder) throws Exception {
-    assume().that(notesMigration.readChanges()).isTrue();
+  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.getId());
     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,12 +889,11 @@
         .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);
     gApi.changes().id(sc.changeId).revision("current").review(in);
@@ -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,7 +987,7 @@
     // 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();
@@ -1049,7 +997,7 @@
     // 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();
@@ -1061,19 +1009,19 @@
   @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
@@ -1094,18 +1043,13 @@
             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 +1062,7 @@
         .cc("nobody2@example.com")
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   /*
@@ -1127,7 +1072,7 @@
   @Test
   public void deleteReviewerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1137,6 +1082,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1152,12 +1098,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.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1167,13 +1114,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.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1183,12 +1131,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.getId());
     removeReviewer(sc, extraCcer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1198,12 +1147,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.getId());
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1211,13 +1161,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 +1177,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 +1192,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.getId());
     removeReviewer(sc, extraReviewer, NotifyHandling.ALL);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1270,15 +1222,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.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1286,19 +1240,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.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend());
   }
 
@@ -1312,13 +1265,16 @@
         ReviewInput.noScore()
             .reviewer(extraReviewer.email)
             .reviewer(extraCcer.email, ReviewerState.CC, false);
-    setApiUser(extraReviewer);
+    requestScopeOperations.setApiUser(extraReviewer.getId());
     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,7 +1282,9 @@
   }
 
   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 {
@@ -1350,7 +1308,7 @@
   public void deleteVoteFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1359,6 +1317,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1366,7 +1325,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1376,13 +1335,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.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1392,6 +1352,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1399,7 +1360,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1409,19 +1370,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.getId());
     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 +1392,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1437,24 +1400,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.getId());
     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.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1462,16 +1427,16 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     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.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1480,13 +1445,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.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1495,6 +1461,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void deleteVote(StagedChange sc, TestAccount account) throws Exception {
@@ -1516,16 +1483,47 @@
    */
 
   @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)) {
+          new TestRepository<>(repo).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 +1538,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1554,6 +1553,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1568,6 +1568,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1580,6 +1581,7 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1587,6 +1589,7 @@
     StagedChange sc = stageChangeReadyForMerge();
     merge(sc.changeId, other, OWNER);
     assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1595,13 +1598,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 +1613,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 +1623,7 @@
   private void merge(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(changeId).revision("current").submit();
   }
 
@@ -1631,7 +1635,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     SubmitInput in = new SubmitInput();
     in.notify = notify;
     gApi.changes().id(changeId).revision("current").submit(in);
@@ -1639,7 +1643,7 @@
 
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.reviewer);
+    requestScopeOperations.setApiUser(sc.reviewer.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     sender.clear();
     return sc;
@@ -1650,8 +1654,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 +1665,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 +1681,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 +1697,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 +1712,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 +1728,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 +1744,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 +1753,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 +1789,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,31 +1804,18 @@
         .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);
@@ -1919,23 +1827,7 @@
         .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
@@ -1943,12 +1835,11 @@
     StagedChange sc = stageWipChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
     pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
-    assertThat(sender).notSent();
+    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);
@@ -1960,28 +1851,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 +1866,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 +1876,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.getIdent(), 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 +1891,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 +1906,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 +1921,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 +1934,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 +1948,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 +1956,7 @@
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2166,27 +1964,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 +1993,7 @@
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, other);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2201,11 +2001,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 +2016,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 +2059,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2284,6 +2073,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2297,6 +2087,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2311,6 +2102,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2325,6 +2117,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2339,6 +2132,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void restore(String changeId, TestAccount by) throws Exception {
@@ -2348,7 +2142,7 @@
   private void restore(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(changeId).restore();
   }
 
@@ -2357,31 +2151,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 +2171,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 +2196,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 +2221,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 +2246,12 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private StagedChange stageChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     gApi.changes().id(sc.changeId).revision("current").submit();
     sender.clear();
@@ -2568,7 +2265,7 @@
   private void revert(StagedChange sc, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(sc.changeId).revert();
   }
 
@@ -2585,6 +2282,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2597,6 +2295,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2608,6 +2307,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2620,25 +2320,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 +2346,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 +2359,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 +2371,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2698,6 +2383,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,7 +2393,7 @@
   private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     AssigneeInput in = new AssigneeInput();
     in.assignee = to.email;
     gApi.changes().id(sc.changeId).setAssignee(in);
@@ -2728,6 +2414,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2743,20 +2430,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.getId());
     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..8ad9f96 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
@@ -92,7 +96,7 @@
     ReviewInput input = new ReviewInput();
     input.message = "Test";
     revision(newChange).review(input);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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..b8380f5 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
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index c8292ba..43a3642 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -18,13 +18,16 @@
 
 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 {
@@ -35,7 +38,7 @@
 
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received only plaintext content
@@ -47,7 +50,7 @@
     assertMailReplyTo(m, user.email);
 
     // Reset user preference
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     i.emailFormat = EmailFormat.HTML_PLAINTEXT;
     gApi.accounts().id(admin.getId().toString()).setPreferences(i);
   }
@@ -56,7 +59,7 @@
   public void userReceivesHtmlAndPlaintextEmail() throws Exception {
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received both HTML and plaintext content
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..1ea64ca 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.getId()), 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..a72cd33 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);
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 8b0c56b..53e21b6 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
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index cfdd781..03f580a 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.getIdent(), 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.getIdent(), 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.getIdent(), 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.getIdent(), testRepo, "private change", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getIdent(), 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.getIdent(), 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.getIdent(), testRepo, "wip change", "a", "a1")
             .to("refs/for/master%wip");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getIdent(), 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.getIdent(), 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.getId());
     watch(watchedProject);
 
     // push a change to watched project -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.getIdent(), 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.getIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -246,9 +247,9 @@
 
   @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.getId());
 
     // 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.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.getIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -278,14 +279,14 @@
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     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.getIdent(), watchedRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -300,19 +301,19 @@
 
   @Test
   public void watchKeyword() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
-    setApiUser(user);
+    String watchedProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch keyword in project as user
     watch(watchedProject, "multimaster");
 
     // push a change with keyword -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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.getIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -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.getIdent(), watchedRepo, "Cleanup cache implementation", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -339,20 +339,18 @@
 
   @Test
   public void watchAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.getId());
 
     // 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.getId());
     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.getIdent(), anyRepo, "TRIGGER", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     // assert email notification
@@ -366,8 +364,8 @@
 
   @Test
   public void watchFileAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.getId());
 
     // 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.getId());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.getIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -395,14 +393,14 @@
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     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.getIdent(), anyRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -417,20 +415,20 @@
 
   @Test
   public void watchKeywordAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
-    setApiUser(user);
+    String anyProject = projectOperations.newProject().create().get();
+    requestScopeOperations.setApiUser(user.getId());
 
     // 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.getId());
     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.getIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -447,7 +445,7 @@
     // notification
     r =
         pushFactory
-            .create(db, admin.getIdent(), anyRepo, "Cleanup cache implementation", "b.txt", "b1")
+            .create(admin.getIdent(), 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.getId());
     watch(watchedProject);
 
     // push a change to watched project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "ignored change", "a", "a1")
+            .create(admin.getIdent(), watchedRepo, "ignored change", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     // ignore the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     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.getId());
     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.getId());
     watch(watchedProject);
 
     // push a private change to watched project -> should not trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "private change", "a", "a1")
+            .create(admin.getIdent(), 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.getId());
     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.getId());
     watch(watchedProject);
 
     // push a private change to watched project -> should trigger email notification for
     // userThatCanViewPrivateChanges, but not for user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index 8abb59d..c61b7ad 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.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).branch("master").reflog();
   }
@@ -98,13 +100,13 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 
   @Test
   public void adminUserIsAllowedToGetReflog() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     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..dea83ca
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -0,0 +1,154 @@
+// 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.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 = createChange().getChange().getId();
+    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 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 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");
+  }
+
+  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..31a8808
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.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.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 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())));
+  }
+}
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..a555ba4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
@@ -0,0 +1,136 @@
+// 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.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 = createChange().getChange().getId();
+    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 = createChange().getChange().getId();
+    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 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..df8c5af 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.getIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     // Approve as admin
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index 98dca3e..53ac70b 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -87,7 +87,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.getIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     ChangeData cd = result1.getChange();
 
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/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/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 237859c..7b0088e 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.Account;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -52,7 +52,7 @@
     adminSshSession.exec(
         String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email, id));
     adminSshSession.assertSuccess();
-    ImmutableSet<Id> reviewers = change.getChange().getReviewers().all();
+    ImmutableSet<Account.Id> reviewers = change.getChange().getReviewers().all();
     if (add) {
       assertThat(reviewers).contains(user.id);
     } else {
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..64dcfc2 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -31,14 +31,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 +227,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();
   }
 
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..ea0bb61
--- /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.getId());
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.getId());
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
+    checkCurrentUser(user.getId());
+  }
+
+  @Test
+  public void setApiUserToExistingUserByTestAccount() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    TestAccount testAccount =
+        accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
+    checkCurrentUser(testAccount.accountId());
+  }
+
+  @Test
+  public void setApiUserToNonExistingUser() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    try {
+      requestScopeOperations.setApiUser(new Account.Id(sequences.nextAccountId()));
+      assert_().fail("expected RuntimeException");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+    checkCurrentUser(admin.getId());
+  }
+
+  @Test
+  public void resetCurrentApiUserClearsCachedState() throws Exception {
+    requestScopeOperations.setApiUser(user.getId());
+    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.getId());
+    assertThat(atrScope.get().getUser().get(key)).isEmpty();
+    assertThat(oldCtx.getUser().get(key)).hasValue("foo");
+  }
+
+  @Test
+  public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    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..29a23c3 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -1,14 +1,15 @@
 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 c586080..a2bd092 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -90,8 +90,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 559b8c7..2f630ad 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -27,17 +27,15 @@
 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 java.util.concurrent.TimeUnit;
+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();
@@ -141,7 +139,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/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 9b1badc..b585960 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 74cb6d2..7854acd 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.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/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index c70f182..25932ce 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 47e9b10..0ce66e8 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 bddbbc9..6972a18 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 5dcf159..988abca 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 54be7b9..534bc36 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 e8b4a2c..1f4653c 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 19b20ed..e6ca7cf 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..cdc94c2
--- /dev/null
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
@@ -0,0 +1,118 @@
+// 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";
+    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/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/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..95c0e85 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -28,6 +28,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 +50,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class PublicKeyStoreTest {
+public class PublicKeyStoreTest extends GerritBaseTests {
   private TestRepository<?> tr;
   private PublicKeyStore store;
 
diff --git a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index 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..fe76d54 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -10,6 +10,7 @@
         "//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",
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..b4f8e7a 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.gerrit.testing.GerritBaseTests;
 import com.google.template.soy.data.SoyMapData;
 import java.net.URISyntaxException;
 import org.junit.Test;
 
-public class IndexServletTest {
+public class IndexServletTest extends GerritBaseTests {
   static class TestIndexServlet extends IndexServlet {
     private static final long serialVersionUID = 1L;
 
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..e3436bc 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -9,6 +9,7 @@
         "//antlr3:query_parser",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
+        "//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/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index 448f292..2175f7d 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -16,10 +16,11 @@
 
 import static org.junit.Assert.assertEquals;
 
+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;
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/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 e4afae2..8e3b71d 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
index af21ad0..47b7509 100644
--- a/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -21,12 +21,12 @@
 
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Provider;
+import com.google.gerrit.testing.GerritBaseTests;
 import java.nio.file.Paths;
 import java.util.Collections;
 import org.junit.Test;
 
-public class LibrariesTest {
+public class LibrariesTest extends GerritBaseTests {
   @Test
   public void create() throws Exception {
     final SitePaths site = new SitePaths(Paths.get("."));
@@ -37,14 +37,7 @@
 
     Libraries lib =
         new Libraries(
-            new Provider<LibraryDownloader>() {
-              @Override
-              public LibraryDownloader get() {
-                return new LibraryDownloader(ui, site, remover);
-              }
-            },
-            Collections.<String>emptyList(),
-            false);
+            () -> new LibraryDownloader(ui, site, remover), Collections.emptyList(), false);
 
     assertNotNull(lib.mysqlDriver);
 
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..326e996
--- /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<?> 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<?> 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/AccountGroupTest.java b/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
deleted file mode 100644
index 18a55bf..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
+++ /dev/null
@@ -1,76 +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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.AccountGroup.UUID.fromRef;
-import static com.google.gerrit.reviewdb.client.AccountGroup.UUID.fromRefPart;
-
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.Month;
-import java.time.ZoneOffset;
-import org.junit.Test;
-
-public class AccountGroupTest {
-  private static final String TEST_UUID = "ccab3195282a8ce4f5014efa391e82d10f884c64";
-  private static final String TEST_SHARDED_UUID = TEST_UUID.substring(0, 2) + "/" + TEST_UUID;
-
-  @Test
-  public void auditCreationInstant() {
-    Instant instant = LocalDateTime.of(2009, Month.JUNE, 8, 19, 31).toInstant(ZoneOffset.UTC);
-    assertThat(AccountGroup.auditCreationInstantTs()).isEqualTo(Timestamp.from(instant));
-  }
-
-  @Test
-  public void parseRefName() {
-    assertThat(fromRef("refs/groups/" + TEST_SHARDED_UUID)).isEqualTo(uuid(TEST_UUID));
-    assertThat(fromRef("refs/groups/" + TEST_SHARDED_UUID + "-2"))
-        .isEqualTo(uuid(TEST_UUID + "-2"));
-    assertThat(fromRef("refs/groups/7e/7ec4775d")).isEqualTo(uuid("7ec4775d"));
-    assertThat(fromRef("refs/groups/fo/foo")).isEqualTo(uuid("foo"));
-
-    assertThat(fromRef(null)).isNull();
-    assertThat(fromRef("")).isNull();
-
-    // Missing prefix.
-    assertThat(fromRef(TEST_SHARDED_UUID)).isNull();
-
-    // Invalid shards.
-    assertThat(fromRef("refs/groups/c/" + TEST_UUID)).isNull();
-    assertThat(fromRef("refs/groups/cca/" + TEST_UUID)).isNull();
-
-    // Mismatched shard.
-    assertThat(fromRef("refs/groups/ca/" + TEST_UUID)).isNull();
-    assertThat(fromRef("refs/groups/64/" + TEST_UUID)).isNull();
-
-    // Wrong number of segments.
-    assertThat(fromRef("refs/groups/cc")).isNull();
-    assertThat(fromRef("refs/groups/" + TEST_SHARDED_UUID + "/1")).isNull();
-  }
-
-  @Test
-  public void parseRefNameParts() {
-    assertThat(fromRefPart(TEST_SHARDED_UUID)).isEqualTo(uuid(TEST_UUID));
-
-    // Mismatched shard.
-    assertThat(fromRefPart("ab/" + TEST_UUID)).isNull();
-  }
-
-  private AccountGroup.UUID uuid(String uuid) {
-    return new AccountGroup.UUID(uuid);
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/client/AccountTest.java b/javatests/com/google/gerrit/reviewdb/client/AccountTest.java
deleted file mode 100644
index 11a562f..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/AccountTest.java
+++ /dev/null
@@ -1,97 +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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRef;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRefPart;
-import static com.google.gerrit.reviewdb.client.Account.Id.fromRefSuffix;
-
-import org.junit.Test;
-
-public class AccountTest {
-  @Test
-  public void parseRefName() {
-    assertThat(fromRef("refs/users/01/1")).isEqualTo(id(1));
-    assertThat(fromRef("refs/users/01/1-drafts")).isEqualTo(id(1));
-    assertThat(fromRef("refs/users/01/1-drafts/2")).isEqualTo(id(1));
-    assertThat(fromRef("refs/users/01/1/edit/2")).isEqualTo(id(1));
-
-    assertThat(fromRef(null)).isNull();
-    assertThat(fromRef("")).isNull();
-
-    // Invalid characters.
-    assertThat(fromRef("refs/users/01a/1")).isNull();
-    assertThat(fromRef("refs/users/01/a1")).isNull();
-
-    // Mismatched shard.
-    assertThat(fromRef("refs/users/01/23")).isNull();
-
-    // Shard too short.
-    assertThat(fromRef("refs/users/1/1")).isNull();
-  }
-
-  @Test
-  public void parseDraftCommentsRefName() {
-    assertThat(fromRef("refs/draft-comments/35/135/1")).isEqualTo(id(1));
-    assertThat(fromRef("refs/draft-comments/35/135/1-foo/2")).isEqualTo(id(1));
-    assertThat(fromRef("refs/draft-comments/35/135/1/foo/2")).isEqualTo(id(1));
-
-    // Invalid characters.
-    assertThat(fromRef("refs/draft-comments/35a/135/1")).isNull();
-    assertThat(fromRef("refs/draft-comments/35/135a/1")).isNull();
-    assertThat(fromRef("refs/draft-comments/35/135/a1")).isNull();
-
-    // Mismatched shard.
-    assertThat(fromRef("refs/draft-comments/02/135/1")).isNull();
-
-    // Shard too short.
-    assertThat(fromRef("refs/draft-comments/2/2/1")).isNull();
-  }
-
-  @Test
-  public void parseStarredChangesRefName() {
-    assertThat(fromRef("refs/starred-changes/35/135/1")).isEqualTo(id(1));
-    assertThat(fromRef("refs/starred-changes/35/135/1-foo/2")).isEqualTo(id(1));
-    assertThat(fromRef("refs/starred-changes/35/135/1/foo/2")).isEqualTo(id(1));
-
-    // Invalid characters.
-    assertThat(fromRef("refs/starred-changes/35a/135/1")).isNull();
-    assertThat(fromRef("refs/starred-changes/35/135a/1")).isNull();
-    assertThat(fromRef("refs/starred-changes/35/135/a1")).isNull();
-
-    // Mismatched shard.
-    assertThat(fromRef("refs/starred-changes/02/135/1")).isNull();
-
-    // Shard too short.
-    assertThat(fromRef("refs/starred-changes/2/2/1")).isNull();
-  }
-
-  @Test
-  public void parseRefNameParts() {
-    assertThat(fromRefPart("01/1")).isEqualTo(id(1));
-    assertThat(fromRefPart("ab/cd")).isNull();
-  }
-
-  @Test
-  public void parseRefSuffix() {
-    assertThat(fromRefSuffix("12/34")).isEqualTo(id(34));
-    assertThat(fromRefSuffix("ab/cd")).isNull();
-  }
-
-  private Account.Id id(int n) {
-    return new Account.Id(n);
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java b/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
deleted file mode 100644
index 6d1d0a6..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ /dev/null
@@ -1,173 +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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import org.junit.Test;
-
-public class ChangeTest {
-  @Test
-  public void parseInvalidRefNames() {
-    assertNotRef(null);
-    assertNotRef("");
-    assertNotRef("01/1/1");
-    assertNotRef("HEAD");
-    assertNotRef("refs/tags/v1");
-  }
-
-  @Test
-  public void parsePatchSetRefNames() {
-    assertRef(1, "refs/changes/01/1/1");
-    assertRef(1234, "refs/changes/34/1234/56");
-
-    // Invalid characters.
-    assertNotRef("refs/changes/0x/1/1");
-    assertNotRef("refs/changes/01/x/1");
-    assertNotRef("refs/changes/01/1/x");
-
-    // Truncations.
-    assertNotRef("refs/changes/");
-    assertNotRef("refs/changes/1");
-    assertNotRef("refs/changes/01");
-    assertNotRef("refs/changes/01/");
-    assertNotRef("refs/changes/01/1/");
-    assertNotRef("refs/changes/01/1/1/");
-    assertNotRef("refs/changes/01//1/1");
-
-    // Leading zeroes.
-    assertNotRef("refs/changes/01/01/1");
-    assertNotRef("refs/changes/01/1/01");
-
-    // Mismatched last 2 digits.
-    assertNotRef("refs/changes/35/1234/56");
-
-    // Something other than patch set after change.
-    assertNotRef("refs/changes/34/1234/0");
-    assertNotRef("refs/changes/34/1234/foo");
-    assertNotRef("refs/changes/34/1234|56");
-    assertNotRef("refs/changes/34/1234foo");
-  }
-
-  @Test
-  public void parseEditRefNames() {
-    assertRef(5, "refs/users/34/1234/edit-5/1");
-    assertRef(5, "refs/users/34/1234/edit-5");
-    assertNotRef("refs/changes/34/1234/edit-5/1");
-    assertNotRef("refs/users/34/1234/EDIT-5/1");
-    assertNotRef("refs/users/34/1234");
-  }
-
-  @Test
-  public void parseChangeMetaRefNames() {
-    assertRef(1, "refs/changes/01/1/meta");
-    assertRef(1234, "refs/changes/34/1234/meta");
-
-    assertNotRef("refs/changes/01/1/met");
-    assertNotRef("refs/changes/01/1/META");
-    assertNotRef("refs/changes/01/1/1/meta");
-  }
-
-  @Test
-  public void parseRobotCommentRefNames() {
-    assertRef(1, "refs/changes/01/1/robot-comments");
-    assertRef(1234, "refs/changes/34/1234/robot-comments");
-
-    assertNotRef("refs/changes/01/1/robot-comment");
-    assertNotRef("refs/changes/01/1/ROBOT-COMMENTS");
-    assertNotRef("refs/changes/01/1/1/robot-comments");
-  }
-
-  @Test
-  public void parseStarredChangesRefNames() {
-    assertAllUsersRef(1, "refs/starred-changes/01/1/1001");
-    assertAllUsersRef(1234, "refs/starred-changes/34/1234/1001");
-
-    assertNotRef("refs/starred-changes/01/1/1001");
-    assertNotAllUsersRef(null);
-    assertNotAllUsersRef("refs/starred-changes/01/1/1xx1");
-    assertNotAllUsersRef("refs/starred-changes/01/1/");
-    assertNotAllUsersRef("refs/starred-changes/01/1");
-    assertNotAllUsersRef("refs/starred-changes/35/1234/1001");
-    assertNotAllUsersRef("refs/starred-changeS/01/1/1001");
-  }
-
-  @Test
-  public void parseDraftRefNames() {
-    assertAllUsersRef(1, "refs/draft-comments/01/1/1001");
-    assertAllUsersRef(1234, "refs/draft-comments/34/1234/1001");
-
-    assertNotRef("refs/draft-comments/01/1/1001");
-    assertNotAllUsersRef(null);
-    assertNotAllUsersRef("refs/draft-comments/01/1/1xx1");
-    assertNotAllUsersRef("refs/draft-comments/01/1/");
-    assertNotAllUsersRef("refs/draft-comments/01/1");
-    assertNotAllUsersRef("refs/draft-comments/35/1234/1001");
-    assertNotAllUsersRef("refs/draft-commentS/01/1/1001");
-  }
-
-  @Test
-  public void toRefPrefix() {
-    assertThat(new Change.Id(1).toRefPrefix()).isEqualTo("refs/changes/01/1/");
-    assertThat(new Change.Id(1234).toRefPrefix()).isEqualTo("refs/changes/34/1234/");
-  }
-
-  @Test
-  public void parseRefNameParts() {
-    assertRefPart(1, "01/1");
-
-    assertNotRefPart(null);
-    assertNotRefPart("");
-
-    // This method assumes that the common prefix "refs/changes/" was removed.
-    assertNotRefPart("refs/changes/01/1");
-
-    // Invalid characters.
-    assertNotRefPart("01a/1");
-    assertNotRefPart("01/a1");
-
-    // Mismatched shard.
-    assertNotRefPart("01/23");
-
-    // Shard too short.
-    assertNotRefPart("1/1");
-  }
-
-  private static void assertRef(int changeId, String refName) {
-    assertThat(Change.Id.fromRef(refName)).isEqualTo(new Change.Id(changeId));
-  }
-
-  private static void assertNotRef(String refName) {
-    assertThat(Change.Id.fromRef(refName)).isNull();
-  }
-
-  private static void assertAllUsersRef(int changeId, String refName) {
-    assertThat(Change.Id.fromAllUsersRef(refName)).isEqualTo(new Change.Id(changeId));
-  }
-
-  private static void assertNotAllUsersRef(String refName) {
-    assertThat(Change.Id.fromAllUsersRef(refName)).isNull();
-  }
-
-  private static void assertRefPart(int changeId, String refName) {
-    assertEquals(new Change.Id(changeId), Change.Id.fromRefPart(refName));
-  }
-
-  private static void assertNotRefPart(String refName) {
-    assertNull(Change.Id.fromRefPart(refName));
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java b/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
deleted file mode 100644
index 5e42ce0..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.testing.GerritBaseTests;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.Test;
-
-public class PatchSetApprovalTest extends GerritBaseTests {
-  @Test
-  public void keyEquality() {
-    PatchSetApproval.Key k1 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("My-Label"));
-    PatchSetApproval.Key k2 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("My-Label"));
-    PatchSetApproval.Key k3 =
-        new PatchSetApproval.Key(
-            new PatchSet.Id(new Change.Id(1), 2), new Account.Id(3), new LabelId("Other-Label"));
-
-    assertThat(k2).isEqualTo(k1);
-    assertThat(k3).isNotEqualTo(k1);
-    assertThat(k2.hashCode()).isEqualTo(k1.hashCode());
-    assertThat(k3.hashCode()).isNotEqualTo(k1.hashCode());
-
-    Map<PatchSetApproval.Key, String> map = new HashMap<>();
-    map.put(k1, "k1");
-    map.put(k2, "k2");
-    map.put(k3, "k3");
-    assertThat(map).containsKey(k1);
-    assertThat(map).containsKey(k2);
-    assertThat(map).containsKey(k3);
-    assertThat(map).containsEntry(k1, "k2");
-    assertThat(map).containsEntry(k2, "k2");
-    assertThat(map).containsEntry(k3, "k3");
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java b/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
deleted file mode 100644
index 51a405f..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ /dev/null
@@ -1,100 +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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.PatchSet.joinGroups;
-import static com.google.gerrit.reviewdb.client.PatchSet.splitGroups;
-
-import com.google.common.collect.ImmutableList;
-import org.junit.Test;
-
-public class PatchSetTest {
-  @Test
-  public void parseRefNames() {
-    assertRef(1, 1, "refs/changes/01/1/1");
-    assertRef(1234, 56, "refs/changes/34/1234/56");
-
-    // Not even close.
-    assertNotRef(null);
-    assertNotRef("");
-    assertNotRef("01/1/1");
-    assertNotRef("HEAD");
-    assertNotRef("refs/tags/v1");
-
-    // Invalid characters.
-    assertNotRef("refs/changes/0x/1/1");
-    assertNotRef("refs/changes/01/x/1");
-    assertNotRef("refs/changes/01/1/x");
-
-    // Truncations.
-    assertNotRef("refs/changes/");
-    assertNotRef("refs/changes/1");
-    assertNotRef("refs/changes/01");
-    assertNotRef("refs/changes/01/");
-    assertNotRef("refs/changes/01/1/");
-    assertNotRef("refs/changes/01/1/1/");
-    assertNotRef("refs/changes/01//1/1");
-
-    // Leading zeroes.
-    assertNotRef("refs/changes/01/01/1");
-    assertNotRef("refs/changes/01/1/01");
-
-    // Mismatched last 2 digits.
-    assertNotRef("refs/changes/35/1234/56");
-
-    // Something other than patch set after change.
-    assertNotRef("refs/changes/34/1234/0");
-    assertNotRef("refs/changes/34/1234/foo");
-    assertNotRef("refs/changes/34/1234|56");
-    assertNotRef("refs/changes/34/1234foo");
-  }
-
-  @Test
-  public void testSplitGroups() {
-    assertThat(splitGroups("")).containsExactly("");
-    assertThat(splitGroups("abcd")).containsExactly("abcd");
-    assertThat(splitGroups("ab,cd")).containsExactly("ab", "cd").inOrder();
-    assertThat(splitGroups("ab,")).containsExactly("ab", "").inOrder();
-    assertThat(splitGroups(",cd")).containsExactly("", "cd").inOrder();
-  }
-
-  @Test
-  public void testJoinGroups() {
-    assertThat(joinGroups(ImmutableList.of(""))).isEqualTo("");
-    assertThat(joinGroups(ImmutableList.of("abcd"))).isEqualTo("abcd");
-    assertThat(joinGroups(ImmutableList.of("ab", "cd"))).isEqualTo("ab,cd");
-    assertThat(joinGroups(ImmutableList.of("ab", ""))).isEqualTo("ab,");
-    assertThat(joinGroups(ImmutableList.of("", "cd"))).isEqualTo(",cd");
-  }
-
-  @Test
-  public void toRefName() {
-    assertThat(new PatchSet.Id(new Change.Id(1), 23).toRefName()).isEqualTo("refs/changes/01/1/23");
-    assertThat(new PatchSet.Id(new Change.Id(1234), 5).toRefName())
-        .isEqualTo("refs/changes/34/1234/5");
-  }
-
-  private static void assertRef(int changeId, int psId, String refName) {
-    assertThat(PatchSet.isChangeRef(refName)).isTrue();
-    assertThat(PatchSet.Id.fromRef(refName))
-        .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
-  }
-
-  private static void assertNotRef(String refName) {
-    assertThat(PatchSet.isChangeRef(refName)).isFalse();
-    assertThat(PatchSet.Id.fromRef(refName)).isNull();
-  }
-}
diff --git a/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java b/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
deleted file mode 100644
index fa6a722..0000000
--- a/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
+++ /dev/null
@@ -1,300 +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.reviewdb.client;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.reviewdb.client.RefNames.parseAfterShardedRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.parseRefSuffix;
-import static com.google.gerrit.reviewdb.client.RefNames.parseShardedRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.parseShardedUuidFromRefPart;
-import static com.google.gerrit.reviewdb.client.RefNames.skipShardedRefPart;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class RefNamesTest {
-  private static final String TEST_GROUP_UUID = "ccab3195282a8ce4f5014efa391e82d10f884c64";
-  private static final String TEST_SHARDED_GROUP_UUID =
-      TEST_GROUP_UUID.substring(0, 2) + "/" + TEST_GROUP_UUID;
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
-  private final Account.Id accountId = new Account.Id(1011123);
-  private final Change.Id changeId = new Change.Id(67473);
-  private final PatchSet.Id psId = new PatchSet.Id(changeId, 42);
-
-  @Test
-  public void fullName() throws Exception {
-    assertThat(RefNames.fullName(RefNames.REFS_CONFIG)).isEqualTo(RefNames.REFS_CONFIG);
-    assertThat(RefNames.fullName("refs/heads/master")).isEqualTo("refs/heads/master");
-    assertThat(RefNames.fullName("master")).isEqualTo("refs/heads/master");
-    assertThat(RefNames.fullName("refs/tags/v1.0")).isEqualTo("refs/tags/v1.0");
-    assertThat(RefNames.fullName("HEAD")).isEqualTo("HEAD");
-  }
-
-  @Test
-  public void changeRefs() throws Exception {
-    String changeMetaRef = RefNames.changeMetaRef(changeId);
-    assertThat(changeMetaRef).isEqualTo("refs/changes/73/67473/meta");
-    assertThat(RefNames.isNoteDbMetaRef(changeMetaRef)).isTrue();
-
-    String robotCommentsRef = RefNames.robotCommentsRef(changeId);
-    assertThat(robotCommentsRef).isEqualTo("refs/changes/73/67473/robot-comments");
-    assertThat(RefNames.isNoteDbMetaRef(robotCommentsRef)).isTrue();
-  }
-
-  @Test
-  public void refForGroupIsSharded() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEFG");
-    String groupRef = RefNames.refsGroups(groupUuid);
-    assertThat(groupRef).isEqualTo("refs/groups/AB/ABCDEFG");
-  }
-
-  @Test
-  public void refForGroupWithUuidLessThanTwoCharsIsRejected() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("A");
-    expectedException.expect(IllegalArgumentException.class);
-    RefNames.refsGroups(groupUuid);
-  }
-
-  @Test
-  public void refForDeletedGroupIsSharded() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEFG");
-    String groupRef = RefNames.refsDeletedGroups(groupUuid);
-    assertThat(groupRef).isEqualTo("refs/deleted-groups/AB/ABCDEFG");
-  }
-
-  @Test
-  public void refForDeletedGroupWithUuidLessThanTwoCharsIsRejected() throws Exception {
-    AccountGroup.UUID groupUuid = new AccountGroup.UUID("A");
-    expectedException.expect(IllegalArgumentException.class);
-    RefNames.refsDeletedGroups(groupUuid);
-  }
-
-  @Test
-  public void refsUsers() throws Exception {
-    assertThat(RefNames.refsUsers(accountId)).isEqualTo("refs/users/23/1011123");
-  }
-
-  @Test
-  public void refsDraftComments() throws Exception {
-    assertThat(RefNames.refsDraftComments(changeId, accountId))
-        .isEqualTo("refs/draft-comments/73/67473/1011123");
-  }
-
-  @Test
-  public void refsDraftCommentsPrefix() throws Exception {
-    assertThat(RefNames.refsDraftCommentsPrefix(changeId))
-        .isEqualTo("refs/draft-comments/73/67473/");
-  }
-
-  @Test
-  public void refsStarredChanges() throws Exception {
-    assertThat(RefNames.refsStarredChanges(changeId, accountId))
-        .isEqualTo("refs/starred-changes/73/67473/1011123");
-  }
-
-  @Test
-  public void refsStarredChangesPrefix() throws Exception {
-    assertThat(RefNames.refsStarredChangesPrefix(changeId))
-        .isEqualTo("refs/starred-changes/73/67473/");
-  }
-
-  @Test
-  public void refsEdit() throws Exception {
-    assertThat(RefNames.refsEdit(accountId, changeId, psId))
-        .isEqualTo("refs/users/23/1011123/edit-67473/42");
-  }
-
-  @Test
-  public void isRefsEdit() throws Exception {
-    assertThat(RefNames.isRefsEdit("refs/users/23/1011123/edit-67473/42")).isTrue();
-
-    // user ref, but no edit ref
-    assertThat(RefNames.isRefsEdit("refs/users/23/1011123")).isFalse();
-
-    // other ref
-    assertThat(RefNames.isRefsEdit("refs/heads/master")).isFalse();
-  }
-
-  @Test
-  public void isRefsUsers() throws Exception {
-    assertThat(RefNames.isRefsUsers("refs/users/23/1011123")).isTrue();
-    assertThat(RefNames.isRefsUsers("refs/users/default")).isTrue();
-    assertThat(RefNames.isRefsUsers("refs/users/23/1011123/edit-67473/42")).isTrue();
-
-    assertThat(RefNames.isRefsUsers("refs/heads/master")).isFalse();
-    assertThat(RefNames.isRefsUsers("refs/groups/" + TEST_SHARDED_GROUP_UUID)).isFalse();
-  }
-
-  @Test
-  public void isRefsGroups() throws Exception {
-    assertThat(RefNames.isRefsGroups("refs/groups/" + TEST_SHARDED_GROUP_UUID)).isTrue();
-
-    assertThat(RefNames.isRefsGroups("refs/heads/master")).isFalse();
-    assertThat(RefNames.isRefsGroups("refs/users/23/1011123")).isFalse();
-    assertThat(RefNames.isRefsGroups(RefNames.REFS_GROUPNAMES)).isFalse();
-    assertThat(RefNames.isRefsGroups("refs/deleted-groups/" + TEST_SHARDED_GROUP_UUID)).isFalse();
-  }
-
-  @Test
-  public void isRefsDeletedGroups() throws Exception {
-    assertThat(RefNames.isRefsDeletedGroups("refs/deleted-groups/" + TEST_SHARDED_GROUP_UUID))
-        .isTrue();
-
-    assertThat(RefNames.isRefsDeletedGroups("refs/heads/master")).isFalse();
-    assertThat(RefNames.isRefsDeletedGroups("refs/users/23/1011123")).isFalse();
-    assertThat(RefNames.isRefsDeletedGroups(RefNames.REFS_GROUPNAMES)).isFalse();
-    assertThat(RefNames.isRefsDeletedGroups("refs/groups/" + TEST_SHARDED_GROUP_UUID)).isFalse();
-  }
-
-  @Test
-  public void isGroupRef() throws Exception {
-    assertThat(RefNames.isGroupRef("refs/groups/" + TEST_SHARDED_GROUP_UUID)).isTrue();
-    assertThat(RefNames.isGroupRef("refs/deleted-groups/" + TEST_SHARDED_GROUP_UUID)).isTrue();
-    assertThat(RefNames.isGroupRef(RefNames.REFS_GROUPNAMES)).isTrue();
-
-    assertThat(RefNames.isGroupRef("refs/heads/master")).isFalse();
-    assertThat(RefNames.isGroupRef("refs/users/23/1011123")).isFalse();
-  }
-
-  @Test
-  public void parseShardedRefsPart() throws Exception {
-    assertThat(parseShardedRefPart("01/1")).isEqualTo(1);
-    assertThat(parseShardedRefPart("01/1-drafts")).isEqualTo(1);
-    assertThat(parseShardedRefPart("01/1-drafts/2")).isEqualTo(1);
-
-    assertThat(parseShardedRefPart(null)).isNull();
-    assertThat(parseShardedRefPart("")).isNull();
-
-    // Prefix not stripped.
-    assertThat(parseShardedRefPart("refs/users/01/1")).isNull();
-
-    // Invalid characters.
-    assertThat(parseShardedRefPart("01a/1")).isNull();
-    assertThat(parseShardedRefPart("01/a1")).isNull();
-
-    // Mismatched shard.
-    assertThat(parseShardedRefPart("01/23")).isNull();
-
-    // Shard too short.
-    assertThat(parseShardedRefPart("1/1")).isNull();
-  }
-
-  @Test
-  public void parseShardedUuidFromRefsPart() throws Exception {
-    assertThat(parseShardedUuidFromRefPart(TEST_SHARDED_GROUP_UUID)).isEqualTo(TEST_GROUP_UUID);
-    assertThat(parseShardedUuidFromRefPart(TEST_SHARDED_GROUP_UUID + "-2"))
-        .isEqualTo(TEST_GROUP_UUID + "-2");
-    assertThat(parseShardedUuidFromRefPart("7e/7ec4775d")).isEqualTo("7ec4775d");
-    assertThat(parseShardedUuidFromRefPart("fo/foo")).isEqualTo("foo");
-
-    assertThat(parseShardedUuidFromRefPart(null)).isNull();
-    assertThat(parseShardedUuidFromRefPart("")).isNull();
-
-    // Prefix not stripped.
-    assertThat(parseShardedUuidFromRefPart("refs/groups/" + TEST_SHARDED_GROUP_UUID)).isNull();
-
-    // Invalid shards.
-    assertThat(parseShardedUuidFromRefPart("c/" + TEST_GROUP_UUID)).isNull();
-    assertThat(parseShardedUuidFromRefPart("cca/" + TEST_GROUP_UUID)).isNull();
-
-    // Mismatched shard.
-    assertThat(parseShardedUuidFromRefPart("ca/" + TEST_GROUP_UUID)).isNull();
-    assertThat(parseShardedUuidFromRefPart("64/" + TEST_GROUP_UUID)).isNull();
-
-    // Wrong number of segments.
-    assertThat(parseShardedUuidFromRefPart("cc")).isNull();
-    assertThat(parseShardedUuidFromRefPart(TEST_SHARDED_GROUP_UUID + "/1")).isNull();
-  }
-
-  @Test
-  public void skipShardedRefsPart() throws Exception {
-    assertThat(skipShardedRefPart("01/1")).isEqualTo("");
-    assertThat(skipShardedRefPart("01/1/")).isEqualTo("/");
-    assertThat(skipShardedRefPart("01/1/2")).isEqualTo("/2");
-    assertThat(skipShardedRefPart("01/1-edit")).isEqualTo("-edit");
-
-    assertThat(skipShardedRefPart(null)).isNull();
-    assertThat(skipShardedRefPart("")).isNull();
-
-    // Prefix not stripped.
-    assertThat(skipShardedRefPart("refs/draft-comments/01/1/2")).isNull();
-
-    // Invalid characters.
-    assertThat(skipShardedRefPart("01a/1/2")).isNull();
-    assertThat(skipShardedRefPart("01a/a1/2")).isNull();
-
-    // Mismatched shard.
-    assertThat(skipShardedRefPart("01/23/2")).isNull();
-
-    // Shard too short.
-    assertThat(skipShardedRefPart("1/1")).isNull();
-  }
-
-  @Test
-  public void parseAfterShardedRefsPart() throws Exception {
-    assertThat(parseAfterShardedRefPart("01/1/2")).isEqualTo(2);
-    assertThat(parseAfterShardedRefPart("01/1/2/4")).isEqualTo(2);
-    assertThat(parseAfterShardedRefPart("01/1/2-edit")).isEqualTo(2);
-
-    assertThat(parseAfterShardedRefPart(null)).isNull();
-    assertThat(parseAfterShardedRefPart("")).isNull();
-
-    // No ID after sharded ref part
-    assertThat(parseAfterShardedRefPart("01/1")).isNull();
-    assertThat(parseAfterShardedRefPart("01/1/")).isNull();
-    assertThat(parseAfterShardedRefPart("01/1/a")).isNull();
-
-    // Prefix not stripped.
-    assertThat(parseAfterShardedRefPart("refs/draft-comments/01/1/2")).isNull();
-
-    // Invalid characters.
-    assertThat(parseAfterShardedRefPart("01a/1/2")).isNull();
-    assertThat(parseAfterShardedRefPart("01a/a1/2")).isNull();
-
-    // Mismatched shard.
-    assertThat(parseAfterShardedRefPart("01/23/2")).isNull();
-
-    // Shard too short.
-    assertThat(parseAfterShardedRefPart("1/1")).isNull();
-  }
-
-  @Test
-  public void testParseRefSuffix() throws Exception {
-    assertThat(parseRefSuffix("1/2/34")).isEqualTo(34);
-    assertThat(parseRefSuffix("/34")).isEqualTo(34);
-
-    assertThat(parseRefSuffix(null)).isNull();
-    assertThat(parseRefSuffix("")).isNull();
-    assertThat(parseRefSuffix("34")).isNull();
-    assertThat(parseRefSuffix("12/ab")).isNull();
-    assertThat(parseRefSuffix("12/a4")).isNull();
-    assertThat(parseRefSuffix("12/4a")).isNull();
-    assertThat(parseRefSuffix("a4")).isNull();
-    assertThat(parseRefSuffix("4a")).isNull();
-  }
-
-  @Test
-  public void shard() throws Exception {
-    assertThat(RefNames.shard(1011123)).isEqualTo("23/1011123");
-    assertThat(RefNames.shard(537)).isEqualTo("37/537");
-    assertThat(RefNames.shard(12)).isEqualTo("12/12");
-    assertThat(RefNames.shard(0)).isEqualTo("00/0");
-    assertThat(RefNames.shard(1)).isEqualTo("01/1");
-    assertThat(RefNames.shard(-1)).isNull();
-  }
-}
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 8b35b6a..39fc2aa 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -38,11 +38,14 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//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",
@@ -53,6 +56,7 @@
         "//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",
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..5f662e9
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -0,0 +1,364 @@
+// 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 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 newAccountResolver().searchImpl(input, searchers, visibilitySupplier);
+  }
+
+  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 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..ddad4b9 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -6,6 +6,7 @@
     deps = [
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:junit",
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/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..75fb94e 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -22,13 +22,14 @@
 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";
 
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 70893a9..f0e9153 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/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/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/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..d02fa1b 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/common/data/testing:common-data-test-util",
         "//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",
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index a5744d1..6f43380 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -32,9 +32,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 +52,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 +112,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 +125,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 +147,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 +226,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 +241,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 +352,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 +362,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 +388,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 +442,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 +452,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 +544,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 +560,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 +627,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 +643,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);
     }
   }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 42d01e2..cff8189 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.common.data.testing.GroupReferenceSubject;
 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 +35,13 @@
 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 +65,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 +103,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 +127,8 @@
     createGroup(groupUuid, groupName);
 
     AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("AnotherGroup");
-    expectedException.expect(OrmDuplicateKeyException.class);
-    expectedException.expectMessage(groupName.get());
+    exception.expect(OrmDuplicateKeyException.class);
+    exception.expectMessage(groupName.get());
     GroupNameNotes.forNewGroup(allUsersName, repo, anotherGroupUuid, groupName);
   }
 
@@ -181,7 +172,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 +181,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 +191,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 +203,8 @@
     AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
     createGroup(anotherGroupUuid, anotherGroupName);
 
-    expectedException.expect(OrmDuplicateKeyException.class);
-    expectedException.expectMessage(anotherGroupName.get());
+    exception.expect(OrmDuplicateKeyException.class);
+    exception.expectMessage(anotherGroupName.get());
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, anotherGroupName);
   }
 
@@ -222,7 +213,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 +223,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);
   }
 
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 53994a6..7117485 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -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..efe6a5a 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -15,23 +15,21 @@
 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));
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index b525504..7a5ed41 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -27,7 +27,7 @@
         new FakeQueryBuilder.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..87e98fd 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
@@ -16,11 +16,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gwtorm.server.OrmException;
 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 {
       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/mail/send/NotificationEmailTest.java b/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
index 885f7cd..5d6fce7 100644
--- a/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.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 NotificationEmailTest {
+public class NotificationEmailTest extends GerritBaseTests {
 
   @Test
   public void getInstanceAndProjectName_returnsTheRightValue() {
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index d00f96b..712449e 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;
@@ -59,11 +58,9 @@
 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 +134,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 +147,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 +157,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 +181,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 +197,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();
+    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..1da4121 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;
 
@@ -45,7 +46,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 +54,12 @@
 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();
@@ -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();
diff --git a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
index dee6181..8440d9f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.junit.TestRepository;
@@ -185,7 +186,6 @@
 
     // 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)))
@@ -297,7 +297,6 @@
     // 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)))
@@ -305,7 +304,6 @@
 
     ImmutableList<RevCommit> newOtherLog =
         log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
-    assertLogEqualExceptTrees(newOtherLog, oldOtherLog);
     assertThat(getLegacyFormatMapForDraftComments(notes, newOtherLog.get(0)))
         .containsExactly(otherCommentPs1.key, false);
   }
@@ -392,7 +390,6 @@
 
     // 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();
@@ -482,23 +479,41 @@
               noteMap,
               status);
       return revNoteMap.revisionNotes.values().stream()
-          .flatMap(crn -> crn.getComments().stream())
+          .flatMap(crn -> crn.getEntities().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)) {
+    try (Repository repo = repoManager.openRepository(project)) {
+      return log(repo, refName);
+    }
+  }
+
+  private ImmutableList<RevCommit> log(Repository repo, String refName) throws Exception {
+    try (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);
+      if (ref == null) {
+        return ImmutableList.of();
+      }
       rw.markStart(rw.parseCommit(ref.getObjectId()));
       return ImmutableList.copyOf(rw);
     }
   }
 
+  private ImmutableListMultimap<String, RevCommit> logAll(
+      Project.NameKey project, Collection<Ref> refs) throws Exception {
+    ImmutableListMultimap.Builder<String, RevCommit> logs = ImmutableListMultimap.builder();
+    try (Repository repo = repoManager.openRepository(project)) {
+      for (Ref r : refs) {
+        logs.putAll(r.getName(), log(repo, r.getName()));
+      }
+    }
+    return logs.build();
+  }
+
   private static void assertLogEqualExceptTrees(
       ImmutableList<RevCommit> actualLog, ImmutableList<RevCommit> expectedLog) {
     assertThat(actualLog).hasSize(expectedLog.size());
@@ -519,9 +534,61 @@
   }
 
   private void assertNoDifferences(ChangeNotes actual, ChangeNotes expected) throws Exception {
-    assertThat(
-            ChangeBundle.fromNotes(commentsUtil, actual)
-                .differencesFrom(ChangeBundle.fromNotes(commentsUtil, expected)))
-        .isEmpty();
+    checkArgument(
+        actual.getChangeId().equals(expected.getChangeId()),
+        "must be same change: %s != %s",
+        actual.getChangeId(),
+        expected.getChangeId());
+
+    // Parsed comment representations are equal.
+    // TODO(dborowitz): Comparing collections directly would be much easier, but Comment doesn't
+    // have a proper equals; switch to that when the issues with
+    // https://gerrit-review.googlesource.com/c/gerrit/+/207013 are resolved.
+    assertCommentsEqual(commentsUtil.draftByChange(actual), commentsUtil.draftByChange(expected));
+    assertCommentsEqual(
+        commentsUtil.publishedByChange(actual), commentsUtil.publishedByChange(expected));
+
+    // Change metadata is equal.
+    assertLogEqualExceptTrees(
+        log(project, actual.getRefName()), log(project, expected.getRefName()));
+
+    // Logs of all draft refs are equal.
+    ImmutableListMultimap<String, RevCommit> actualDraftLogs =
+        logAll(allUsersName, commentsUtil.getDraftRefs(actual.getChangeId()));
+    ImmutableListMultimap<String, RevCommit> expectedDraftLogs =
+        logAll(allUsersName, commentsUtil.getDraftRefs(expected.getChangeId()));
+    assertThat(actualDraftLogs.keySet())
+        .named("draft ref names")
+        .containsExactlyElementsIn(expectedDraftLogs.keySet());
+    for (String refName : actualDraftLogs.keySet()) {
+      assertLogEqualExceptTrees(actualDraftLogs.get(refName), actualDraftLogs.get(refName));
+    }
+  }
+
+  private static void assertCommentsEqual(List<Comment> actualList, List<Comment> expectedList) {
+    ImmutableMap<Comment.Key, Comment> actualMap = byKey(actualList);
+    ImmutableMap<Comment.Key, Comment> expectedMap = byKey(expectedList);
+    assertThat(actualMap.keySet()).isEqualTo(expectedMap.keySet());
+    for (Comment.Key key : actualMap.keySet()) {
+      Comment actual = actualMap.get(key);
+      Comment expected = expectedMap.get(key);
+      assertThat(actual.key).isEqualTo(expected.key);
+      assertThat(actual.lineNbr).isEqualTo(expected.lineNbr);
+      assertThat(actual.author).isEqualTo(expected.author);
+      assertThat(actual.getRealAuthor()).isEqualTo(expected.getRealAuthor());
+      assertThat(actual.writtenOn).isEqualTo(expected.writtenOn);
+      assertThat(actual.side).isEqualTo(expected.side);
+      assertThat(actual.message).isEqualTo(expected.message);
+      assertThat(actual.parentUuid).isEqualTo(expected.parentUuid);
+      assertThat(actual.range).isEqualTo(expected.range);
+      assertThat(actual.tag).isEqualTo(expected.tag);
+      assertThat(actual.revId).isEqualTo(expected.revId);
+      assertThat(actual.serverId).isEqualTo(expected.serverId);
+      assertThat(actual.unresolved).isEqualTo(expected.unresolved);
+    }
+  }
+
+  private static ImmutableMap<Comment.Key, Comment> byKey(List<Comment> comments) {
+    return comments.stream().collect(toImmutableMap(c -> c.key, c -> c));
   }
 }
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..1abaa22
--- /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.git.LockFailureException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gwtorm.server.OrmException;
+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 OrmException");
+    } catch (OrmException 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..5aad6ff 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -26,6 +26,7 @@
 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;
@@ -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;
 
@@ -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.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.next();
   }
 
@@ -335,11 +332,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.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.increaseTo(2);
   }
 
@@ -352,7 +349,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..0cc4b00 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.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 a3f9f93..3a9037c 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -46,7 +46,6 @@
 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/*");
@@ -951,7 +930,6 @@
   }
 
   private InMemoryRepository add(ProjectConfig pc) {
-    SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
 
     InMemoryRepository repo;
@@ -966,7 +944,6 @@
     all.put(
         pc.getName(),
         new ProjectState(
-            sitePaths,
             projectCache,
             allProjectsName,
             allUsersName,
@@ -986,8 +963,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..fd81507 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,247 @@
   @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"));
+
+    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:\"\"", 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 +1613,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 +1773,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 +1787,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 +1802,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");
@@ -1721,6 +1861,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 +1892,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 +1933,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));
@@ -1810,19 +1964,7 @@
     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));
-    }
-
-    indexer.index(db, project, id);
+    indexer.index(project, id);
     assertQuery("draftby:" + userId);
   }
 
@@ -1915,9 +2057,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 +2176,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 +2239,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 +2298,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 +2461,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 +2470,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 +2497,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 +2657,7 @@
       return this;
     }
 
-    DashboardChangeState draftAndDeleteCommentBy(Id commenterId) {
+    DashboardChangeState draftAndDeleteCommentBy(Account.Id commenterId) {
       deleteDraftCommentBy.add(commenterId);
       return this;
     }
@@ -3125,6 +3094,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 +3161,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 +3236,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 +3252,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 +3281,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 +3325,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 +3403,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 +3432,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 fc1483c..1ccac32 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -61,6 +61,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//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",
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index c8637cd..f7252c0 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");
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..9944a42 100644
--- a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -19,11 +19,12 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gwtorm.server.OrmException;
 import java.util.Arrays;
 import org.junit.Test;
 
-public class RegexPathPredicateTest {
+public class RegexPathPredicateTest extends GerritBaseTests {
   @Test
   public void prefixOnlyOptimization() throws OrmException {
     RegexPathPredicate p = predicate("^a/b/.*");
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..b23219b
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -0,0 +1,301 @@
+// 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.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 com.google.gwtorm.server.OrmException;
+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 OrmException");
+    } catch (OrmException 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 OrmException");
+    } catch (OrmException 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);
+      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 OrmException, IOException {
+        try (Repository repo = repoManager.createRepository(allProjectsName)) {
+          if (initialVersion.isPresent()) {
+            TestRepository<?> tr = new TestRepository<>(repo);
+            tr.update(RefNames.REFS_VERSION, tr.blob(initialVersion.get().toString()));
+          }
+        } catch (Exception e) {
+          throw new OrmException(e);
+        }
+        repoManager.createRepository(allUsersName).close();
+        setUp();
+      }
+
+      @Override
+      public void ensureCreated() throws OrmException, 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() throws OrmException {
+      new RepoSequence(
+              repoManager,
+              GitReferenceUpdated.DISABLED,
+              allUsersName,
+              Sequences.NAME_GROUPS,
+              () -> 1,
+              1)
+          .next();
+    }
+
+    /**
+     * Test-specific setup.
+     *
+     * @throws OrmException if an error occurs.
+     */
+    protected void setUp() throws OrmException {}
+
+    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() throws OrmException {
+            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() throws OrmException {
+            seedGroupSequenceRef();
+          }
+        };
+    try {
+      u.update();
+      assert_().fail("expected OrmException");
+    } catch (OrmException 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 OrmException");
+    } catch (OrmException 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..a0176de
--- /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.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+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 OrmException");
+    } catch (OrmException 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 OrmException");
+    } catch (OrmException 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..e4089c5
--- /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<?> 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<?> 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..9117e85 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",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 70c4383..2ae0a5f 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -17,10 +17,10 @@
 import static org.junit.Assert.assertEquals;
 
 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;
@@ -55,7 +54,7 @@
     final RevCommit masterCommit = repo.branch("master").commit().create();
     final 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
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..096afa6 100644
--- a/javatests/com/google/gerrit/server/util/git/BUILD
+++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -10,6 +10,7 @@
     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",
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/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 213c8c5..0000000
--- a/javatests/org/eclipse/jgit/BUILD
+++ /dev/null
@@ -1,11 +0,0 @@
-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 95ca4db..8221e13 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -27,20 +27,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"],
@@ -80,6 +66,13 @@
 )
 
 java_library(
+    name = "guava-failureaccess",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@guava-failureaccess//jar"],
+)
+
+java_library(
     name = "j2objc",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
@@ -91,6 +84,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
     exports = [
+        ":guava-failureaccess",
         ":j2objc",
         "@guava//jar",
     ],
@@ -497,10 +491,3 @@
     visibility = ["//visibility:public"],
     exports = ["@icu4j//jar"],
 )
-
-java_library(
-    name = "postgresql",
-    data = ["//lib:LICENSE-postgresql"],
-    visibility = ["//visibility:public"],
-    exports = ["@postgresql//jar"],
-)
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-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/codemirror/BUILD b/lib/codemirror/BUILD
deleted file mode 100644
index d0c9278..0000000
--- a/lib/codemirror/BUILD
+++ /dev/null
@@ -1,11 +0,0 @@
-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 593caa3..0000000
--- a/lib/codemirror/cm.bzl
+++ /dev/null
@@ -1,375 +0,0 @@
-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],
-        )
-
-        native.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 93d3c2f..e8de396 100644
--- a/lib/commons/BUILD
+++ b/lib/commons/BUILD
@@ -44,6 +44,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..b1bbca1
--- /dev/null
+++ b/lib/gitiles/BUILD
@@ -0,0 +1,56 @@
+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 062f866..9cbd0eb 100644
--- a/lib/greenmail/BUILD
+++ b/lib/greenmail/BUILD
@@ -1,8 +1,22 @@
 package(default_visibility = ["//visibility:public"])
 
+POST_JDK8_DEPS = [":javax-activation"]
+
+java_library(
+    name = "javax-activation",
+    testonly = True,
+    data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+    exports = ["@javax-activation//jar"],
+)
+
 java_library(
     name = "greenmail",
     testonly = True,
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@greenmail//jar"],
+    runtime_deps = select({
+        "//:java9": POST_JDK8_DEPS,
+        "//:java_next": POST_JDK8_DEPS,
+        "//conditions:default": [],
+    }),
 )
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 fa2fef3..0000000
--- a/lib/gwt/BUILD
+++ /dev/null
@@ -1,45 +0,0 @@
-[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/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 7120c21..b3b73ad 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.6.201903130242-r"
+_JGIT_VERS = "5.2.1.201812262042-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
@@ -40,28 +40,28 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "c33db190745a8a40524d3076f033ba2ee4616a64",
-        src_sha1 = "f0e33171d3d413c5a94f07794aef7b71a35772f6",
+        sha1 = "34914e63e1463e40ba40e2e28b0392993ea3b938",
+        src_sha1 = "b1c9e2ae01dd31ab4957de54756ec11acc99bb30",
         unsign = True,
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "86564c102e81a76f67630d4fde862d137e24c63a",
+        sha1 = "18c8938c4d8966abed84fc9de6c09aaea8cc8d87",
         unsign = True,
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "f4ebfa91f4c988f37648b3087830a83973a975be",
+        sha1 = "08c945bc664e4efe0d0e9a878f96505076da2ca9",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "b2e512e920afbac405a678efe92e3b4bacb9a1df",
+        sha1 = "5a5fb36517cb05ca51cbb1f00a520142dc83f793",
         unsign = True,
     )
 
diff --git a/lib/mockito/BUILD b/lib/mockito/BUILD
new file mode 100644
index 0000000..df7537b
--- /dev/null
+++ b/lib/mockito/BUILD
@@ -0,0 +1,35 @@
+package(
+    default_testonly = True,
+    default_visibility = ["//visibility:private"],
+)
+
+java_library(
+    name = "mockito",
+    data = ["//lib:LICENSE-Apache2.0"],
+    # 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/plugins/BUILD b/plugins/BUILD
index 2b0c0d4..5e5dbbf 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -32,9 +32,11 @@
     "//java/com/google/gerrit/common:annotations",
     "//java/com/google/gerrit/common:server",
     "//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",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index c4cf42b..65f3cba 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit c4cf42b96a049a0fb854bcbcb85b56a82d91a009
+Subproject commit 65f3cbac2174c7aad65a3d7c9325a66871ae5752
diff --git a/plugins/delete-project b/plugins/delete-project
new file mode 160000
index 0000000..d689466
--- /dev/null
+++ b/plugins/delete-project
@@ -0,0 +1 @@
+Subproject commit d689466ad51bf95fc955c4639ce8e12d9b8c14b1
diff --git a/plugins/gitiles b/plugins/gitiles
new file mode 160000
index 0000000..dcd8d7f
--- /dev/null
+++ b/plugins/gitiles
@@ -0,0 +1 @@
+Subproject commit dcd8d7f93ebe21e60688e65fa1261a836a82eb32
diff --git a/plugins/hooks b/plugins/hooks
index 47e4b87..4928874 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 47e4b87fca52c5b1dea9ab37833e36fb4f57d3fb
+Subproject commit 492887449bf66b0f828aa26506629a8f5838f4d3
diff --git a/plugins/replication b/plugins/replication
index 31b419c..ff75ac8 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 31b419cbd7126e2c9397ec5abd0ad975ed937d75
+Subproject commit ff75ac8a4c806e5f627cb755f49e08962fa6e6b0
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 5ff3620..d7ba499 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 5ff36200b29567118b3aede8e49ba0b6c6b1adb1
+Subproject commit d7ba4997b5c0038c85f6393b73a9ca16ca4fb927
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index ae9a9ab..ff77708 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit ae9a9ab370d8026ad809a73b4dae3fb7d79ad8a4
+Subproject commit ff777080a881e85060ab446ae3f00612bcfb5ae1
diff --git a/plugins/webhooks b/plugins/webhooks
new file mode 160000
index 0000000..ed81825
--- /dev/null
+++ b/plugins/webhooks
@@ -0,0 +1 @@
+Subproject commit ed818254ca32196c0e0acf4090b7b44a918f25ee
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 3c21a42..6244512 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -71,9 +71,9 @@
 that serves PolyGerrit:
 
 ```sh
-bazel build polygerrit &&
+bazel build gerrit &&
   $(bazel info output_base)/external/local_jdk/bin/java \
-  -jar bazel-bin/polygerrit.war daemon --polygerrit-dev \
+  -jar bazel-bin/gerrit.war daemon --polygerrit-dev \
   -d ../gerrit_testsite --console-log --show-stack-trace
 ```
 
@@ -123,7 +123,9 @@
 ./polygerrit-ui/run-server.sh
 ```
 
-Then visit http://localhost:8081/elements/foo/bar_test.html
+Then visit http://localhost:8081/elements/foo/bar_test.html and check "Disable
+cache" in the "Network" tab of Chrome's dev tools, so code changes are picked
+up on "reload".
 
 To run Chrome tests in headless mode:
 
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index b5d3dae..97151f2 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -63,7 +63,6 @@
     "object-shorthand": ["error", "always"],
     "prefer-arrow-callback": "error",
     "prefer-const": "error",
-    "prefer-promise-reject-errors": "off",
     "prefer-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
     "require-jsdoc": "off",
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..4b670da 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
@@ -66,24 +66,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/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
index fb0c685..e949a87 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',
+          },
           viewDrafts: {
             id: 'viewDrafts',
             name: 'View Drafts',
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 d4e83d9..7e68669 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
@@ -232,7 +232,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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index af982cf..4266b22 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/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 81123e7..b86d971 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/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 8c152b6..02a2a08 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
@@ -222,6 +222,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..72ea3f0 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
@@ -57,6 +57,9 @@
         type: String,
         computed: '_computeChangeSize(change)',
       },
+      _dynamicCellEndpoints: {
+        type: Array,
+      },
     },
 
     behaviors: [
@@ -67,6 +70,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/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 61c64ff..765b49d 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
@@ -63,7 +63,7 @@
        * properties should not be used together.
        *
        * @type {!Array<{
-       *   sectionName: string,
+       *   name: string,
        *   query: string,
        *   results: !Array<!Object>
        * }>}
@@ -76,6 +76,9 @@
         type: Array,
         computed: '_computeLabelNames(sections)',
       },
+      _dynamicHeaderEndpoints: {
+        type: Array,
+      },
       selectedIndex: {
         type: Number,
         notify: true,
@@ -128,6 +131,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
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-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-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 56bc17c..0625bbe 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
@@ -207,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 3a3454d..618ec65 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
@@ -279,7 +279,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..e31f9ad
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -0,0 +1,29 @@
+/**
+ * @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',
+    properties: {
+      account: Object,
+      sections: Array,
+      preferences: Object,
+      showNewUserHelp: Boolean,
+    },
+  });
+})();
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 81fe54e..1d83819 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-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 07573d3..472a187 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;
@@ -355,6 +356,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 +522,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 +561,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 +590,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 +610,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 +630,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]]">
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 470f859..123a7b5 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',
@@ -106,6 +108,14 @@
         type: Boolean,
         value: false,
       },
+      disableDiffPrefs: {
+        type: Boolean,
+        value: false,
+      },
+      _diffPrefsDisabled: {
+        type: Boolean,
+        computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+      },
       _commentThreads: Array,
       /** @type {?} */
       _serverConfig: {
@@ -248,6 +258,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 +335,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 +404,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 +506,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 +538,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 +743,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 +777,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 +897,10 @@
       return parentCount > 1 && preferFirst ? -1 : 'PARENT';
     },
 
+    _computeShowPrimaryTabs(dynamicTabContentEndpoints) {
+      return dynamicTabContentEndpoints.length > 0;
+    },
+
     _computeChangeUrl(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
@@ -1014,6 +1075,8 @@
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
 
+      if (this._diffPrefsDisabled) { return; }
+
       e.preventDefault();
       this.$.fileList.openDiffPrefs();
     },
@@ -1711,5 +1774,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 419cf01..b8b48e7 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-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..cefa045 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
@@ -35,6 +35,7 @@
     properties: {
       /**
        * @type {{
+       *    is_private: boolean,
        *    subject: string,
        *  }}
        */
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 c6e369c..7b79a63 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
@@ -62,6 +62,7 @@
       serverConfig: Object,
       shownFileCount: Number,
       diffPrefs: Object,
+      diffPrefsDisabled: Boolean,
       diffViewMode: {
         type: String,
         notify: true,
@@ -80,6 +81,10 @@
         type: String,
         value: '',
       },
+      showTitle: {
+        type: Boolean,
+        value: true,
+      },
       _descriptionReadOnly: {
         type: Boolean,
         computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
@@ -183,11 +188,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 fba7b1b..460512a 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
@@ -218,7 +218,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,
       };
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 a441965..83e5216 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
@@ -1366,7 +1366,7 @@
         id: '503008e2_0ab203ee',
         line: 10,
         updated: '2018-02-14 22:07:43.000000000',
-        message: 'response',
+        message: 'a comment',
         unresolved: true,
       },
       {
@@ -1375,7 +1375,7 @@
         line: 20,
         in_reply_to: 'ecf0b9fa_fe1a5f62',
         updated: '2018-02-13 22:07:43.000000000',
-        message: 'a comments',
+        message: 'response',
         unresolved: true,
       },
     ];
@@ -1410,6 +1410,7 @@
         ignore_whitespace: 'IGNORE_NONE',
       };
       diff._diff = mock.diffResponse;
+      diff.$.diff.flushDebouncer('renderDiffTable');
     };
 
     const renderAndGetNewDiffs = function(index) {
@@ -1712,11 +1713,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',
@@ -1727,6 +1760,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,
@@ -1735,57 +1769,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-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 19b0716b..32c4c1f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -132,9 +132,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;
       }
@@ -227,12 +230,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..8ecd6b0 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -24,17 +24,17 @@
     is: 'gr-message',
 
     /**
-     * Fired when this message's permalink is tapped.
-     *
-     * @event scroll-to
-     */
-
-    /**
      * Fired when this message's reply link is tapped.
      *
      * @event reply
      */
 
+    /**
+     * Fired when the message's timestamp is tapped.
+     *
+     * @event message-anchor-tap
+     */
+
     listeners: {
       tap: '_handleTap',
     },
@@ -223,22 +223,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 89a3523..023431c 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
@@ -184,8 +184,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-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_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/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..16a0fd0 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
@@ -283,6 +283,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-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index 3650707..b1433a3 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,
@@ -353,9 +355,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;
         }
@@ -369,6 +373,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 36126e4..59af36d 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',
@@ -191,7 +196,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 ?
@@ -210,7 +215,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()) {
@@ -229,10 +234,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);
@@ -452,6 +455,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 8b85074..2087790 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 c590bcc..af4c345 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -393,6 +393,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}`;
@@ -633,7 +636,7 @@
           return Promise.resolve();
         } else {
           this._redirectToLogin(data.canonicalPath);
-          return Promise.reject();
+          return Promise.reject(new Error());
         }
       });
     },
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 7a92402..4dbcd44 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
@@ -327,6 +327,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', () => {
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 91c16c3..17f883f 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:',
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 9a04b1f..a9b1660 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 cc66e3b..9646b06 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
@@ -16,8 +16,7 @@
 -->
 <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,7 +28,7 @@
     </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>
@@ -39,6 +38,7 @@
     <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,16 +51,6 @@
     (function() {
       'use strict';
 
-      const Defs = {};
-
-      /**
-       * @typedef {{
-       *  number: number,
-       *  leftSide: {boolean}
-       * }}
-       */
-      Defs.LineOfInterest;
-
       const DiffViewMode = {
         SIDE_BY_SIDE: 'SIDE_BY_SIDE',
         UNIFIED: 'UNIFIED_DIFF',
@@ -91,12 +81,6 @@
          */
 
         /**
-         * Fired when the diff is rendered.
-         *
-         * @event render
-         */
-
-        /**
          * Fired when the diff finishes rendering text content, but not
          * necessarily syntax highlights.
          *
@@ -109,26 +93,29 @@
           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,63 @@
           '_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 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}));
+          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;
+                    }
+
+                    reporting.timeEnd(TimingLabel.CONTENT);
+                    reporting.time(TimingLabel.SYNTAX);
+                    return this.$.syntaxLayer.process().then(() => {
+                      reporting.timeEnd(TimingLabel.SYNTAX);
+                      reporting.timeEnd(TimingLabel.TOTAL);
+                    });
+                  }));
+          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 +198,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 +274,10 @@
         cancel() {
           this.$.processor.cancel();
           this.$.syntaxLayer.cancel();
+          if (this._cancelableRenderPromise) {
+            this._cancelableRenderPromise.cancel();
+            this._cancelableRenderPromise = null;
+          }
         },
 
         _handlePreferenceError(pref) {
@@ -294,7 +290,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 +302,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 +325,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 +342,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 +356,7 @@
                     highlight.endIndex;
 
                 GrAnnotation.annotateElement(
-                    el,
+                    contentEl,
                     highlight.startIndex,
                     endIndex - highlight.startIndex,
                     HL_CLASS);
@@ -401,7 +368,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 +379,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 +395,7 @@
           }.bind(this);
 
           return {
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               if (!show()) { return; }
 
               const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
@@ -438,7 +405,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,25 +413,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 4b85c22..c8d91df 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 = [];
@@ -221,7 +218,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);
     }
   };
@@ -319,152 +318,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');
@@ -492,7 +345,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');
@@ -509,7 +363,7 @@
     }
 
     for (const layer of this.layers) {
-      layer.annotate(contentText, line);
+      layer.annotate(contentText, lineNumberEl, line);
     }
 
     td.appendChild(contentText);
@@ -743,5 +597,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 3238cbc..67d8c19 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 = {
@@ -1098,9 +789,9 @@
         });
         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,7 +803,7 @@
           return builder;
         });
         element.diff = {content};
-        element.render({left: [], right: []}, prefs).then(done);
+        element.render(keyLocations, prefs).then(done);
       });
 
       test('reporting', done => {
@@ -1137,7 +828,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 +850,13 @@
         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');
           done();
         });
       });
@@ -1178,10 +868,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 +875,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 +895,7 @@
       let builder;
       let diff;
       let prefs;
+      let keyLocations;
 
       setup(done => {
         element = fixture('mock-diff');
@@ -1217,8 +907,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 +965,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 +976,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 +994,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 +1067,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 +1087,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 d5e6855..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ /dev/null
@@ -1,465 +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 b525a60..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 90d465f..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 aee7a62..860d900 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
@@ -293,7 +293,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 af8725e..6419a7c 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
@@ -21,7 +21,11 @@
     is: 'gr-diff-highlight',
 
     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 +33,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 +49,86 @@
       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() {
+    /**
+     * 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.
+     */
+    handleSelectionChange(selection, isMouseUp) {
       // 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);
+      this.debounce(
+          'selectionChange', () => this._handleSelection(selection, isMouseUp),
+          200);
     },
 
-    _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));
     },
 
     /**
@@ -107,6 +136,7 @@
      * Merges multiple ranges, accounts for triple click, accounts for
      * syntax highligh, convert native DOM Range objects to Gerrit concepts
      * (line, side, etc).
+     * @param {Selection} selection
      * @return {({
      *   start: {
      *     node: Node,
@@ -122,8 +152,7 @@
      *   }
      * })|null|!Object}
      */
-    _getNormalizedRange() {
-      const selection = window.getSelection();
+    _getNormalizedRange(selection) {
       const rangeCount = selection.rangeCount;
       if (rangeCount === 0) {
         return null;
@@ -235,7 +264,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;
@@ -268,12 +297,12 @@
       actionBox.placeBelow(range);
     },
 
-    _handleSelection() {
-      const normalizedRange = this._getNormalizedRange();
+    _handleSelection(selection, isMouseUp) {
+      const normalizedRange = this._getNormalizedRange(selection);
       if (!normalizedRange) {
         return;
       }
-      const domRange = window.getSelection().getRangeAt(0);
+      const domRange = selection.getRangeAt(0);
       /** @type {?} */
       const start = normalizedRange.start;
       if (!start) {
@@ -291,14 +320,32 @@
 
       // TODO (viktard): Drop empty first and last lines from selection.
 
+      // 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.
+      // Rather than trying to find the line contents, we just check if the
+      // selection is empty to see that it's at the end of a line.
+      // In this case, we select the entire start line.
+      if (isMouseUp && start.line === end.line - 1 && end.column === 0) {
+        const content = domRange.cloneContents().querySelector('.contentText');
+        if (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;
+        }
+      }
+
       const 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) {
@@ -317,7 +364,7 @@
       }
     },
 
-    _createComment(e) {
+    _createRangeComment(e) {
       this._removeActionBox();
     },
 
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 b10e3cc..9ec2b29 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 e3bf866..4c310b9 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 6f61fb9..fd460cc 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
@@ -45,13 +45,18 @@
     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',
 
@@ -84,9 +89,6 @@
       prefs: {
         type: Object,
       },
-      projectConfig: {
-        type: Object,
-      },
       projectName: String,
       displayLine: {
         type: Boolean,
@@ -111,7 +113,10 @@
         type: Boolean,
         value: false,
       },
-      comments: Object,
+      comments: {
+        type: Object,
+        observer: '_commentsChanged',
+      },
       lineWrapping: {
         type: Boolean,
         value: false,
@@ -174,10 +179,28 @@
       },
 
       _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',
     },
 
     observers: [
@@ -299,9 +322,12 @@
       this._blame = null;
     },
 
-    /** @return {!Array<!HTMLElement>} */
+    /**
+     * The thread elements in this diff, in no particular order.
+     * @return {!Array<!HTMLElement>}
+     */
     getThreadEls() {
-      return this.$.diff.getThreadEls();
+      return Polymer.dom(this.$.diff).querySelectorAll('.comment-thread');
     },
 
     /** @param {HTMLElement} el */
@@ -428,6 +454,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}
@@ -445,11 +534,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.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.
@@ -506,5 +744,86 @@
         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);
+    },
   });
 })();
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 f83253e..ab9daec 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,14 @@
   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; },
+      });
       element = fixture('basic');
     });
 
@@ -47,6 +52,215 @@
       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);
+    });
+
     test('reload() cancels before network resolves', () => {
       const cancelStub = sandbox.stub(element.$.diff, 'cancel');
 
@@ -59,12 +273,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 +391,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 +769,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 +829,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 +866,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 +976,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-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 dead8d7..a9268a7 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
@@ -80,8 +80,18 @@
         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,
     },
 
@@ -108,6 +118,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());
 
@@ -115,57 +129,64 @@
       // 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.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._splitCommonGroupsWithComments(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.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();
       }
     },
 
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 27e467d..6a9d88f 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
@@ -83,7 +83,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 06658da..57525e1 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
@@ -286,11 +286,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
@@ -332,10 +332,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 89ad2d2..c8b3c4b 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
@@ -69,6 +69,14 @@
         value() { return {}; },
         observer: '_changeViewStateChanged',
       },
+      disableDiffPrefs: {
+        type: Boolean,
+        value: false,
+      },
+      _diffPrefsDisabled: {
+        type: Boolean,
+        computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+      },
       /** @type {?} */
       _patchRange: Object,
       /** @type {?} */
@@ -213,7 +221,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,
       };
@@ -457,6 +465,7 @@
     _handleCommaKey(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
+      if (this._diffPrefsDisabled) { return; }
 
       e.preventDefault();
       this.$.diffPreferencesDialog.open();
@@ -803,8 +812,8 @@
           (unresolvedString ? `${unresolvedString}` : '');
     },
 
-    _computePrefsButtonHidden(prefs, loggedIn) {
-      return !loggedIn || !prefs;
+    _computePrefsButtonHidden(prefs, prefsDisabled) {
+      return prefsDisabled || !prefs;
     },
 
     _handleFileChange(e) {
@@ -1010,6 +1019,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 354bfef..c4d6c95 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.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index bddfc6d..4ccdf96 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);
@@ -276,10 +280,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]]"
@@ -289,10 +293,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 791d771..1c018de 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,6 +102,8 @@
    */
   const COMMIT_MSG_LINE_LENGTH = 72;
 
+  const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+
   Polymer({
     is: 'gr-diff',
 
@@ -64,16 +119,17 @@
      */
 
     /**
-     * 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.
-      *
-      * @event draft-interaction
-      */
+    /**
+     * Fired when rendering, including syntax highlighting, is done. Also fired
+     * when no rendering can be done because required preferences are not set.
+     *
+     * @event render
+     */
 
     properties: {
       changeNum: String,
@@ -88,10 +144,6 @@
         type: Object,
         observer: '_prefsObserver',
       },
-      projectConfig: {
-        type: Object,
-        observer: '_projectConfigChanged',
-      },
       projectName: String,
       displayLine: {
         type: Boolean,
@@ -106,9 +158,10 @@
         reflectToAttribute: true,
       },
       noRenderOnPrefsChange: Boolean,
-      comments: {
-        type: Object,
-        value: {left: [], right: []},
+      /** @type {!Array<!Gerrit.HoveredRange>} */
+      _commentRanges: {
+        type: Array,
+        value: () => [],
       },
       lineWrapping: {
         type: Boolean,
@@ -121,13 +174,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: {
@@ -186,27 +233,32 @@
         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: [
@@ -214,15 +266,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>} */
@@ -252,22 +424,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'];
@@ -332,10 +488,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)) {
@@ -369,88 +525,51 @@
 
     /**
      * @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;
     },
 
@@ -501,75 +620,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;
@@ -631,19 +681,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;
@@ -657,7 +723,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.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);
+      }
     },
 
     /**
@@ -671,16 +785,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;
@@ -701,12 +809,12 @@
 
     _handleFullBypass() {
       this._safetyBypass = FULL_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     _handleLimitedBypass() {
       this._safetyBypass = LIMITED_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     /** @return {string} */
@@ -722,16 +830,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 faf529b..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-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index db14fc8..426c871 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,40 +17,44 @@
 (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',
 
     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.*)',
     ],
 
     /**
      * Layer method to add annotations to a line.
      * @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 &&
@@ -93,97 +97,81 @@
     },
 
     /**
-     * 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.
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..abf8e73 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
@@ -23,7 +23,7 @@
     /**
      * Fired when the comment creation action was taken (hotkey, click).
      *
-     * @event create-comment
+     * @event create-range-comment
      */
 
     properties: {
@@ -34,10 +34,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 +63,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 +74,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 +117,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 8bf42b7..32eb969 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
@@ -83,6 +83,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',
@@ -154,6 +155,16 @@
       },
       /** @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,
     },
 
@@ -165,9 +176,10 @@
      * Annotation layer method to add syntax annotations to the given element
      * 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.
@@ -197,12 +209,23 @@
       }
     },
 
+    _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.
      * @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 = [];
@@ -211,13 +234,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();
@@ -232,49 +253,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() {
@@ -348,7 +375,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);
@@ -356,7 +384,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/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 b34cd0a..0301a13 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -42,6 +42,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">
@@ -220,10 +221,6 @@
         <a class="feedback"
             href$="[[_feedbackUrl]]"
             rel="noopener" target="_blank">Send feedback</a>
-        <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
       </div>
     </footer>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 321dc58..f2f06d1 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -331,11 +331,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',
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/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index 13b3152..0a2ae78 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-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 8607948..1785d1f 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/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 8efd309..3afbe54 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,8 +26,8 @@
   };
 
   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).';
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..11fce6d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -0,0 +1,493 @@
+/**
+ * @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',
+
+    /**
+     * 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..2e1b3bd
--- /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..a576583
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -0,0 +1,648 @@
+/**
+ * @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',
+
+    /**
+     * 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/diff/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
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
rename to polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
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-js-api-interface/gr-annotation-actions-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js
index 9331173..45f28d1 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
@@ -19,15 +19,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;
@@ -36,16 +40,28 @@
   }
 
   /**
-   * Method to add annotations to a 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')
+   * Method to add annotations to a content line.
+   * @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 47281c2..5f7b8ff 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.
    */
@@ -158,13 +159,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 820d2c0..f981c99 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 = {
@@ -69,6 +70,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 +192,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..113a6f7 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');
@@ -441,28 +452,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 9931f72..8832a3f 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-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 36a428d..b31c3f2 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
@@ -189,14 +189,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 +239,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) {
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 ef8c112..478c619 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
@@ -82,6 +82,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-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 09eff80..849972f 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
@@ -353,7 +353,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) {
@@ -1321,6 +1321,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,
@@ -1350,30 +1352,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) {
@@ -2506,7 +2510,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 => {
@@ -2666,12 +2672,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;
           });
     },
@@ -2701,12 +2707,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/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/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index 9bec658..f8b5560 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/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/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index ec445ae..e326e97 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -133,6 +133,8 @@
   --syntax-selector-attr-color: #FA8602;
   --syntax-template-tag-color: #FA8602;
   --syntax-param-color: var(--primary-text-color);
+
+  --reply-overlay-z-index: 1000;
 }
 @media screen and (max-width: 50em) {
   :root {
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 6037a88..f22ec5d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -80,6 +80,8 @@
       --syntax-selector-attr-color: #80CBBF;
       --syntax-template-tag-color: #C792EA;
 
+      --reply-overlay-z-index: 1000;
+
       background-color: var(--view-background-color);
     }
   </style>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index 92b99e3..c5979fa 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -60,3 +60,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 5784ead..d9e1238 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -104,9 +104,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',
@@ -158,6 +155,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/proto/BUILD b/proto/BUILD
index 00d725a..cef28a1 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -9,23 +9,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..c978069 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
 }
 
 
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/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 9801b44..e4f699c 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? deprecateGwtUi: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
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..55e6ef5 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}
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/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 724e90d..d869205 100644
--- a/resources/com/google/gerrit/server/mail/DeleteVote.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteVote.soy
@@ -19,11 +19,11 @@
 /**
  * The .DeleteVote template will determine the contents of the email related
  * to removing votes on changes.
- * @param change
- * @param coverLetter
- * @param fromName
  */
 {template .DeleteVote kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param fromName: ?}
   {$fromName} has removed a vote on this change.{\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/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 40924e6..04d54c4 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 and it was merged.
   {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 b11c5e5..e8c04a5 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>merged</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 f11edfe..84a3075 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 aefb867..ddd2a51 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -62,7 +62,7 @@
         "-Xep:LiteralClassName:WARN",
         "-Xep:MissingFail:WARN",
         "-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 d3e5033..0000000
--- a/tools/bzl/gwt.bzl
+++ /dev/null
@@ -1,312 +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] + ctx.files._zip,
-        outputs = [gwt_user_agent_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 = []
-    for dep in deps:
-        paths.append(dep.path)
-
-    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 = list(deps) + gwt_user_agent_modules,
-        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.java.transitive_runtime_deps)
-        deps.append(dep.java.transitive_source_jars)
-    for dep in ctx.attr.deps:
-        if hasattr(dep, "java"):
-            deps.append(dep.java.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/junit.bzl b/tools/bzl/junit.bzl
index 8ab635c..5da5f05 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -68,7 +68,6 @@
     # Enforce JDK 8 compatibility on Java 9, see
     # https://docs.oracle.com/javase/9/intl/internationalization-enhancements-jdk-9.htm#JSINT-GUID-AF5AECA7-07C1-4E7D-BC10-BC7E73DC6C7F
     "-Djava.locale.providers=COMPAT,CLDR,SPI",
-    "--add-modules java.activation",
     "--add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED",
 ]
 
@@ -81,8 +80,8 @@
     )
     jvm_flags = kwargs.get("jvm_flags", [])
     jvm_flags = jvm_flags + select({
-        "//:java10": POST_JDK8_OPTS,
         "//:java9": POST_JDK8_OPTS,
+        "//:java_next": POST_JDK8_OPTS,
         "//conditions:default": [],
     })
     native.java_test(
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 86cd215..a480908 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 5ae7dd9..066fe43 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,13 +1,4 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load(
-    "//tools/bzl:gwt.bzl",
-    "GWT_COMPILER_ARGS",
-    "GWT_JVM_ARGS",
-    "GWT_PLUGIN_DEPS",
-    "GWT_PLUGIN_DEPS_NEVERLINK",
-    "GWT_TRANSITIVE_DEPS",
-    "gwt_binary",
-)
 
 PLUGIN_DEPS = ["//plugins:plugin-lib"]
 
@@ -25,7 +16,6 @@
         deps = [],
         provided_deps = [],
         srcs = [],
-        gwt_module = [],
         resources = [],
         manifest_entries = [],
         dir_name = None,
@@ -35,14 +25,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
@@ -58,34 +46,6 @@
         **kwargs
     )
 
-    if gwt_module:
-        native.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..fdb4c1d 100644
--- a/tools/bzl/plugins.bzl
+++ b/tools/bzl/plugins.bzl
@@ -1,11 +1,14 @@
 CORE_PLUGINS = [
     "codemirror-editor",
     "commit-message-length-validator",
+    "delete-project",
     "download-commands",
+    "gitiles",
     "hooks",
     "replication",
     "reviewnotes",
     "singleusergroup",
+    "webhooks",
 ]
 
 CUSTOM_PLUGINS = [
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 5d27c47..b8bfe16 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -8,31 +8,19 @@
 )
 
 TEST_DEPS = [
-    "//gerrit-gwtui:ui_tests",
     "//javatests/com/google/gerrit/elasticsearch:elasticsearch_test_utils",
     "//javatests/com/google/gerrit/server:server_tests",
 ]
 
 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(
@@ -50,11 +38,6 @@
 )
 
 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 ce4baf9..e9e249f 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -27,7 +27,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',
@@ -37,7 +36,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',
 }
 
@@ -176,8 +174,6 @@
     src = set()
     lib = set()
     proto = set()
-    gwt_src = set()
-    gwt_lib = set()
     plugins = set()
 
     # Classpath entries are absolute for cross-cell support
@@ -185,10 +181,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 = path.join(ext, p)
-            gwt_lib.add(p)
             continue
 
         m = java_library.match(p)
@@ -214,11 +206,6 @@
                 p = 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')
@@ -250,7 +237,7 @@
                 if 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)
@@ -263,16 +250,6 @@
             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):
@@ -280,11 +257,6 @@
         s = s.replace('.jar', '-src.jar')
         classpathentry('lib', p, s)
 
-    for s in sorted(gwt_src):
-        p = path.join(ROOT, s, 'src', 'main', 'java')
-        if path.exists(p):
-            classpathentry('lib', p, out='eclipse-out/gwtsrc')
-
     classpathentry('con', JRE)
     classpathentry('output', 'eclipse-out/classes')
 
@@ -326,14 +298,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 path.isdir(gwt_working_dir):
-        makedirs(path.join(ROOT, gwt_working_dir))
-
     try:
-        check_call(_build_bazel_cmd('build', MAIN, GWT,
-                                    '//java/org/eclipse/jgit:libEdit-src.jar'))
+        check_call(_build_bazel_cmd('build', MAIN))
     except CalledProcessError:
         exit(1)
 except KeyboardInterrupt:
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 ddab402..6a8d53d 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.8-SNAPSHOT</version>
+  <version>3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index db023c6..363b915 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.8-SNAPSHOT</version>
+  <version>3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index db25800..7ef1276 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.8-SNAPSHOT</version>
+  <version>3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-plugin-gwtui_pom.xml b/tools/maven/gerrit-plugin-gwtui_pom.xml
deleted file mode 100644
index cf5d624..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.8-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 0d2addc..7249bfc 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.8-SNAPSHOT</version>
+  <version>3.0-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 2a4b7cb..20fd8a7 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.8-SNAPSHOT"
+GERRIT_VERSION = "3.0-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>