Merge "maven_jar: Add support to consume snapshot dependencies"
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 4ce7b5f..010b292 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -18,6 +18,11 @@
 	url = ../plugins/download-commands
 	branch = .
 
+[submodule "plugins/gitiles"]
+	path = plugins/gitiles
+	url = ../plugins/gitiles
+	branch = .
+
 [submodule "plugins/hooks"]
 	path = plugins/hooks
 	url = ../plugins/hooks
diff --git a/.mailmap b/.mailmap
index cbf1f3b..b5c119c 100644
--- a/.mailmap
+++ b/.mailmap
@@ -6,12 +6,14 @@
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex.ryazantsev <alex.ryazantsev@gmail.com>
 Andrew Bonventre <andybons@chromium.org>                                                    <andybons@google.com>
 Becky Siegel <beckysiegel@google.com>                                                       beckysiegel <beckysiegel@google.com>
+Ben Rohlfs <brohlfs@google.com>                                                             brohlfs <brohlfs@google.com>
 Brad Larson <bklarson@gmail.com>                                                            <brad.larson@garmin.com>
 Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonyericsson.com>
 Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonymobile.com>
 Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
 Changcheng Xiao <xchangcheng@google.com>                                                    xchangcheng
 Dariusz Luksza <dluksza@collab.net>                                                         <dariusz@luksza.org>
+Darrien Glasser <darrien@arista.com>                                                        darrien <darrien@arista.com>
 Dave Borowitz <dborowitz@google.com>                                                        <dborowitz@google.com>
 David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
 David Ostrovsky <david@ostrovsky.org>                                                       <david.ostrovsky@gmail.com>
diff --git a/BUILD b/BUILD
index 72e557c..3989a75 100644
--- a/BUILD
+++ b/BUILD
@@ -1,8 +1,8 @@
-package(default_visibility = ["//visibility:public"])
-
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("//tools/bzl:pkg_war.bzl", "pkg_war")
 
+package(default_visibility = ["//visibility:public"])
+
 config_setting(
     name = "java9",
     values = {
diff --git a/Documentation/BUILD b/Documentation/BUILD
index 8a7a313..52ab7a8 100644
--- a/Documentation/BUILD
+++ b/Documentation/BUILD
@@ -1,10 +1,8 @@
-package(default_visibility = ["//visibility:public"])
-
-load("//tools/bzl:asciidoc.bzl", "documentation_attributes")
-load("//tools/bzl:asciidoc.bzl", "genasciidoc")
-load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
+load("//tools/bzl:asciidoc.bzl", "documentation_attributes", "genasciidoc", "genasciidoc_zip")
 load("//tools/bzl:license.bzl", "license_map")
 
+package(default_visibility = ["//visibility:public"])
+
 exports_files([
     "replace_macros.py",
 ])
@@ -58,9 +56,20 @@
     ],
 )
 
+sh_test(
+    name = "check_licenses",
+    srcs = ["check_licenses_test.sh"],
+    data = [
+        "js_licenses.gen.txt",
+        "js_licenses.txt",
+        "licenses.gen.txt",
+        "licenses.txt",
+    ],
+)
+
 DOC_DIR = "Documentation"
 
-SRCS = glob(["*.txt"]) + [":licenses.txt"]
+SRCS = glob(["*.txt"])
 
 genrule(
     name = "index",
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index c514321..cdf6b30 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1089,21 +1089,43 @@
 [[block]]
 === 'BLOCK' access rule
 
-The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK'
-rule cannot be overridden in the inheriting project. Any 'ALLOW' rule
-from an inheriting project, which conflicts with an inherited 'BLOCK'
-rule will not be honored. Searching for 'BLOCK' rules, in the chain
-of parent projects, ignores the Exclusive flag, unless the rule with
-the Exclusive flag is defined on the same project as the 'BLOCK'
-rule. This means within the same project a 'BLOCK' rule can be
-overruled by 'ALLOW' rules on the same access section and 'ALLOW'
-rules with Exclusive flag on access section for more specific refs.
+The 'BLOCK' rule can be used to take away rights from users. The BLOCK rule
+works across project inheritance, from the top down, so an administrator can
+use 'BLOCK' rules to enforce site-wide restrictions.
+
+For example, if a user in the 'Foo Users' group tries to push to
+'refs/heads/mater' with the permissions below, that user will be blocked
+
+[options="header"]
+|=========================================================================
+|Project      | Inherits From    |Reference Name |Permissions            |
+|All-Projects | -                |refs/*         |push = block Foo Users |
+|Foo          | All-Projects     |refs/heads/*   |push = Foo Users       |
+|=========================================================================
+
+'BLOCK' rules are evaluated starting from the parent project, and after a 'BLOCK'
+rule is found to apply, further rules are ignored. Hence, in this example, the
+permissions on child-project is ignored.
+
+----
+All-Projects: project.config
+  [access "refs/heads/*"]
+    push = block group X
+
+child-project: project.config
+  [access "refs/heads/*"]
+    exclusiveGroupPermissions = push
+    push = group X
+----
+
+In this case push for group 'X' will be blocked, even though the Exclusive
+flag was set for the child-project.
 
 A 'BLOCK' rule that blocks the 'push' permission blocks any type of push,
 force or not. A blocking force push rule blocks only force pushes, but
 allows non-forced pushes if an 'ALLOW' rule would have permitted it.
 
-It is also possible to block label ranges.  To block a group 'X' from voting
+It is also possible to block label ranges. To block a group 'X' from voting
 '-2' and '+2', but keep their existing voting permissions for the '-1..+1'
 range intact we would define:
 
@@ -1130,6 +1152,24 @@
 In this case a user which is a member of the group 'Y' will still be allowed to
 push to 'refs/heads/*' even if it is a member of the group 'X'.
 
+=== 'BLOCK' and 'ALLOW' rules in the same project with the Exclusive flag
+
+When a project contains a 'BLOCK' and 'ALLOW' that uses the Exclusive flag in a
+more specific reference, the 'ALLOW' rule with the Exclusive flag will override
+the 'BLOCK' rule:
+
+----
+  [access "refs/*"]
+    read = block group X
+
+  [access "refs/heads/*"]
+    exclusiveGroupPermissions = read
+    read = group X
+----
+
+In this case a user which is a member of the group 'X' will still be allowed to
+read 'refs/heads/*'.
+
 [NOTE]
 An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
 inside the same access section of the same project. An 'ALLOW' rule in a
@@ -1358,7 +1398,8 @@
 any of their groups is used.
 
 This limit applies not only to the link:cmd-query.html[`gerrit query`]
-command, but also to the web UI results pagination size.
+command, but also to the web UI results pagination size in the new
+PolyGerrit UI and, limited to the full project list, in the old GWT UI.
 
 
 [[capability_readAs]]
diff --git a/Documentation/check_licenses_test.sh b/Documentation/check_licenses_test.sh
new file mode 100755
index 0000000..a65a827
--- /dev/null
+++ b/Documentation/check_licenses_test.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+hook=$(pwd)/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+
+for f in  js_licenses licenses ; do
+  if ! diff -u Documentation/${f}.txt Documentation/${f}.gen.txt  ; then
+     echo ""
+     echo "FAIL: ${f}.txt out of date"
+     echo "to fix: "
+     echo ""
+     echo "  cp bazel-genfiles/Documentation/${f}.gen.txt Documentation/${f}.txt"
+     echo ""
+     exit 1
+  fi
+done
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 026d7b1..9e3d70b 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -28,9 +28,8 @@
 == DESCRIPTION
 Creates a new bare Git repository under `gerrit.basePath`, using
 the project name supplied.  The newly created repository is empty
-(has no commits), but is registered in the Gerrit database so that
-the initial commit may be uploaded for review, or initial content
-can be pushed directly into a branch.
+(has no commits), and the initial content may either be uploaded for
+review, or pushed directly to a branch.
 
 If replication is enabled, this command also connects to each of
 the configured remote systems over SSH and uses command line git
@@ -119,7 +118,7 @@
 Defaults to MERGE_IF_NECESSARY unless
 link:config-gerrit.html#repository.name.defaultSubmitType[
 repository.<name>.defaultSubmitType] is set to a different value.
-For more details see link:project-configuration.html#submit_type[
+For more details see link:config-project-config.html#submit-type[
 Submit Types].
 
 --use-content-merge::
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index 55d9083..5a84b9d 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -16,7 +16,7 @@
 truth when it needs the information again.
 
 Flushing a cache may be necessary if an administrator modifies
-database records directly in the database, rather than going through
+NoteDb metadata directly in a repository, rather than going through
 the Gerrit web interface.
 
 If no options are supplied, defaults to `--all`.
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index e999218..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`.'
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 3bb8e4f..fb35dc2 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -14,6 +14,7 @@
   [--format {text | json | json_compact}]
   [--all]
   [--limit <N>]
+  [--prefix | -p <prefix>]
   [--has-acl-for GROUP]
 --
 
@@ -87,6 +88,9 @@
 --limit::
 	Cap the number of results to the first N matches.
 
+--prefix::
+	Limit the results to those projects that start with the specified prefix.
+
 --has-acl-for::
 	Display only projects on which access rights for this group are
 	directly assigned. Projects which only inherit access rights for
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 5417901..eef47fc 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -15,19 +15,17 @@
   [--abandon | --restore]
   [--rebase]
   [--move <BRANCH>]
-  [--publish]
   [--json | -j]
-  [--delete]
   [--verified <N>] [--code-review <N>]
   [--label Label-Name=<N>]
   [--tag TAG]
-  {COMMIT | CHANGEID,PATCHSET}...
+  {COMMIT | CHANGENUMBER,PATCHSET}...
 --
 
 == 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.
@@ -66,7 +64,7 @@
 	Read review input json from stdin. See
 	link:rest-api-changes.html#review-input[ReviewInput] entity for the
 	format.
-	(option is mutually exclusive with --submit, --restore, --publish, --delete,
+	(option is mutually exclusive with --submit, --restore,
 	--abandon, --message, --rebase and --move)
 
 --notify::
@@ -88,7 +86,7 @@
 
 --abandon::
 	Abandon the specified change(s).
-	(option is mutually exclusive with --submit, --restore, --publish, --delete,
+	(option is mutually exclusive with --submit, --restore,
 	--rebase, --move and --json)
 
 --restore::
@@ -97,7 +95,7 @@
 
 --rebase::
 	Rebase the specified change(s).
-	(option is mutually exclusive with --abandon, --submit, --delete and --json)
+	(option is mutually exclusive with --abandon, --submit and --json)
 
 --move::
 	Move the specified change(s).
@@ -106,7 +104,7 @@
 --submit::
 -s::
 	Submit the specified patch set(s) for merging.
-	(option is mutually exclusive with --abandon, --publish --delete, --rebase
+	(option is mutually exclusive with --abandon, --rebase
 	and --json)
 
 --code-review::
@@ -144,19 +142,24 @@
 
 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"
+----
+$ ssh -p 29418 review.example.com gerrit review --code-review +2 8242,2
 ----
 
 Vote on the project specific label "mylabel":
 ----
-$ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 c0ff33
+$ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 8242,2
 ----
 
 Append the message "Build Successful". Notice two levels of quoting is
 required, one for the local shell, and another for the argument parser
 inside the Gerrit server:
 ----
-$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' c0ff33
+$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' 8242,2
 ----
 
 Mark the unmerged commits both "Verified +1" and "Code-Review +2" and
@@ -172,7 +175,7 @@
 
 Abandon an active change:
 ----
-$ ssh -p 29418 review.example.com gerrit review --abandon c0ff33
+$ ssh -p 29418 review.example.com gerrit review --abandon 8242,2
 ----
 
 == SEE ALSO
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index 45b31ff..9686230 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -59,7 +59,7 @@
 
 +
 For more details see
-link:project-configuration.html#submit_type[Submit Types].
+link:config-project-config.html#submit-type[Submit Types].
 
 --content-merge::
     If enabled, Gerrit will try to perform a 3-way merge of text
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 08b661d..8f24a47 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -280,6 +280,8 @@
 
 change:: link:json.html#change[change attribute]
 
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
 changer:: link:json.html#account[account attribute]
 
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
@@ -294,6 +296,8 @@
 
 change:: link:json.html#change[change attribute]
 
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
 changer:: link:json.html#account[account attribute]
 
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index 51d8cec..13e663e 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -200,7 +200,7 @@
 * link:rest-api-accounts.html#edit-preferences-info[Edit Preferences]
 
 If the value for a preference is the same as the default value for this
-preference, it can be omitted in the `preference.config` file.
+preference, it can be omitted in the `preferences.config` file.
 
 Defaults for preferences that apply for all accounts can be configured
 in the `refs/users/default` branch in the `All-Users` repository.
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-gerrit.txt b/Documentation/config-gerrit.txt
index 94ee7ab..ee02e1b 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]
@@ -805,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"`::
 +
@@ -819,23 +818,18 @@
 requires two HTTP requests, and this cache tries to carry state from
 the first request into the second to ensure it can complete.
 
-cache `"change_refs"`::
-+
-Cache entries are used to compute change ref visibility efficiently. Entries
-contain the minimal required information for this task.
+cache `"changes"`::
 +
 The size of `memoryLimit` determines the number of projects for which
 all changes will be cached. If the cache is set to 1024, this means all
-at maximum 1024 changes can be held in the cache.
+changes for up to 1024 projects can be held in the cache.
 +
-Default value is 10.000.
+Default value is 0 (disabled). It is disabled by default due to the fact
+that change updates are not communicated between Gerrit servers. Hence
+this cache should be disabled in an multi-master/multi-slave setup.
 +
-If the size is set to 0, the cache is disabled. The change index will not
-be used at all. Instead, the change notes cache is used directly.
-+
-A good size for this cache is twice the number of changes that the Gerrit
-instance has to allow it to hold all current changes and account for
-growth.
+The cache should be flushed whenever the database changes table is modified
+outside of Gerrit.
 
 cache `"diff"`::
 +
@@ -985,6 +979,11 @@
 Caches parsed `rules.pl` contents for each project. This cache uses the same
 size as the `projects` cache, and cannot be configured independently.
 
+cache `"pure_revert"`::
++
+Result of checking if one change or commit is a pure/clean revert of
+another.
+
 cache `"sshkeys"`::
 +
 Caches unpacked versions of user SSH keys, so the internal SSH daemon
@@ -1146,42 +1145,23 @@
 [[change]]
 === Section change
 
-[[change.largeChange]]change.largeChange::
-+
-Number of changed lines from which on a change is considered as a large
-change. The number of changed lines of a change is the sum of the lines
-that were inserted and deleted in the change.
-+
-The specified value is used to visualize the change sizes in the Web UI
-in change tables and user dashboards.
-+
-By default 500.
-
-[[change.updateDelay]]change.updateDelay::
-+
-How often in seconds the web interface should poll for updates to the
-currently open change.  The poller relies on the client's browser
-cache to use If-Modified-Since and respect `304 Not Modified` HTTP
-responses.  This allows for fast polls, often under 8 milliseconds.
-+
-With a configured 30 second delay a server with 4900 active users will
-typically need to dedicate 1 CPU to the update check.  4900 users
-divided by an average delay of 30 seconds is 163 requests arriving per
-second.  If requests are served at \~6 ms response time, 1 CPU is
-necessary to keep up with the update request traffic.  On a smaller
-user base of 500 active users, the default 30 second delay is only 17
-requests per second and requires ~10% CPU.
-+
-If 0 the update polling is disabled.
-+
-Default is 5 minutes.
-
 [[change.allowBlame]]change.allowBlame::
 +
 Allow blame on side by side diff. If set to false, blame cannot be used.
 +
 Default is true.
 
+[[change.allowDrafts]]change.allowDrafts::
++
+Legacy support for drafts workflow. If set to true, pushing a new change
+with draft option will create a private change. Pushing with draft option
+to an existing change will create change edit.
++
+Enabling this option allows to push to the `refs/drafts/branch`. When
+disabled any push to `refs/drafts/branch` will be rejected.
++
+Default is false.
+
 [[change.api.allowedIdentifier]]change.api.allowedIdentifier::
 +
 Change identifier(s) that are allowed on the API. See
@@ -1194,14 +1174,12 @@
 +
 Default is `ALL`.
 
-[[change.allowDrafts]]change.allowDrafts::
+[[change.api.excludeMergeableInChangeInfo]]change.api.excludeMergeableInChangeInfo::
 +
-Legacy support for drafts workflow. If set to true, pushing a new change
-with draft option will create a private change. Pushing with draft option
-to an existing change will create change edit.
-+
-Enabling this option allows to push to the `refs/drafts/branch`. When
-disabled any push to `refs/drafts/branch` will be rejected.
+If true, the mergeability bit in
+link:rest-api-changes.html#change-info[ChangeInfo] will never be set. It can
+be requested separately through the
+link:rest-api-changes.html#get-mergeable[get-mergeable] endpoint.
 +
 Default is false.
 
@@ -1220,6 +1198,67 @@
 +
 Default is true.
 
+[[change.disablePrivateChanges]]change.disablePrivateChanges::
++
+If set to true, users are not allowed to create private changes.
++
+The default is false.
+
+[[change.largeChange]]change.largeChange::
++
+Number of changed lines from which on a change is considered as a large
+change. The number of changed lines of a change is the sum of the lines
+that were inserted and deleted in the change.
++
+The specified value is used to visualize the change sizes in the Web UI
+in change tables and user dashboards.
++
+By default 500.
+
+[[change.maxUpdates]]change.maxUpdates::
++
+Maximum number of updates to a change. Counts only updates to the main NoteDb
+meta ref; draft comments, robot comments, stars, etc. do not count towards the
+total.
++
+Many NoteDb operations require walking the entire change meta ref and loading
+its contents into memory, so changes with arbitrarily many updates may cause
+high CPU usage, memory pressure, persistent cache bloat, and other problems.
++
+The following operations are allowed even when a change is at the limit:
+* Abandon
+* Submit
+* Submit by push with `%submit`
+* Auto-close by pushing directly to the branch
+* Fix with link:rest-api-changes.html#fix-input[`expect_merged_as`]
++
+By default 1000.
+
+[[change.replyLabel]]change.replyLabel::
++
+Label name for the reply button. In the user interface an ellipsis (…)
+is appended.
++
+Default is "Reply". In the user interface it becomes "Reply…".
+
+[[change.replyTooltip]]change.replyTooltip::
++
+Tooltip for the reply button. In the user interface a note about the
+keyboard shortcut is appended.
++
+Default is "Reply and score". In the user interface it becomes "Reply
+and score (Shortcut: a)".
+
+[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
++
+Maximum allowed size of a robot comment that will be accepted. Robot comments
+which exceed the indicated size will be rejected on addition. The specified
+value is interpreted as the maximum size in bytes of the JSON representation of
+the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported.
+Zero or negative values allow robot comments of unlimited size.
++
+The default limit is 1024kB.
+
 [[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
 +
 Show assignee field in changes table. If set to false, assignees will
@@ -1227,6 +1266,14 @@
 +
 Default is false.
 
+[[change.strictLabels]]change.strictLabels::
++
+Reject invalid label votes: invalid labels or invalid values. This
+configuration option is provided for backwards compaitbility and may
+be removed in future gerrit versions.
++
+Default is false.
+
 [[change.submitLabel]]change.submitLabel::
 +
 Label name for the submit button.
@@ -1260,13 +1307,6 @@
 Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
 changes including ancestors and other changes related by topic)".
 
-[[change.submitWholeTopic]]change.submitWholeTopic::
-+
-Determines if the submit button submits the whole topic instead of
-just the current change.
-+
-Default is false.
-
 [[change.submitTopicLabel]]change.submitTopicLabel::
 +
 If `change.submitWholeTopic` is set and a change has a topic,
@@ -1287,44 +1327,31 @@
 (${submitSize} changes including ancestors and other
 changes related by topic)".
 
-[[change.replyLabel]]change.replyLabel::
+[[change.submitWholeTopic]]change.submitWholeTopic::
 +
-Label name for the reply button. In the user interface an ellipsis (…)
-is appended.
-+
-Default is "Reply". In the user interface it becomes "Reply…".
-
-[[change.replyTooltip]]change.replyTooltip::
-+
-Tooltip for the reply button. In the user interface a note about the
-keyboard shortcut is appended.
-+
-Default is "Reply and score". In the user interface it becomes "Reply
-and score (Shortcut: a)".
-
-[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
-+
-Maximum allowed size of a robot comment that will be accepted. Robot comments
-which exceed the indicated size will be rejected on addition. The specified
-value is interpreted as the maximum size in bytes of the JSON representation of
-the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported.
-Zero or negative values allow robot comments of unlimited size.
-+
-The default limit is 1024kB.
-
-[[change.strictLabels]]change.strictLabels::
-+
-Reject invalid label votes: invalid labels or invalid values. This
-configuration option is provided for backwards compaitbility and may
-be removed in future gerrit versions.
+Determines if the submit button submits the whole topic instead of
+just the current change.
 +
 Default is false.
 
-[[change.disablePrivateChanges]]change.disablePrivateChanges::
+[[change.updateDelay]]change.updateDelay::
 +
-If set to true, users are not allowed to create private changes.
+How often in seconds the web interface should poll for updates to the
+currently open change.  The poller relies on the client's browser
+cache to use If-Modified-Since and respect `304 Not Modified` HTTP
+responses.  This allows for fast polls, often under 8 milliseconds.
 +
-The default is false.
+With a configured 30 second delay a server with 4900 active users will
+typically need to dedicate 1 CPU to the update check.  4900 users
+divided by an average delay of 30 seconds is 163 requests arriving per
+second.  If requests are served at \~6 ms response time, 1 CPU is
+necessary to keep up with the update request traffic.  On a smaller
+user base of 500 active users, the default 30 second delay is only 17
+requests per second and requires ~10% CPU.
++
+If 0 the update polling is disabled.
++
+Default is 5 minutes.
 
 [[changeCleanup]]
 === Section changeCleanup
@@ -1964,6 +1991,39 @@
   installModule = com.example.abc.OurSpecialSauceModule
 ----
 
+[[gerrit.listProjectsFromIndex]]gerrit.listProjectsFromIndex::
++
+Enable rendering of project list from the secondary index instead
+of purely relying on the in-memory cache.
++
+By default false.
++
+[NOTE]
+The in-memory cache (set to false) rendering provides an **unlimited list** as a result
+of the list project API, causing the full list of projects to be
+returned as a result of the link:rest-api-projects.html[/projects/] REST API
+or the link:cmd-ls-projects.html[gerrit ls-projects] SSH command.
+When the rendering from the secondary index (set to true),
+the **list is limited** by the global capability
+link:access-control.html#capability_queryLimit[queryLimit]
+which is defaulted to 500 entries.
+
+[[gerrit.primaryWeblinkName]]gerrit.primaryWeblinkName::
++
+Name of the link:dev-plugins.html#links-to-external-tools[Weblink] that should
+be chosen in cases where only one Weblink can be used in the UI, for example in
+inline links.
++
+By default unset, meaning that the UI is responsible for trying to identify
+a weblink to be used in these cases, most likely weblinks that links to code
+browsers with known integrations with Gerrit (like Gitiles and Gitweb).
++
+Example:
+----
+[gerrit]
+  primaryWeblinkName = gitiles
+----
+
 [[gerrit.reportBugUrl]]gerrit.reportBugUrl::
 +
 URL to direct users to when they need to report a bug.
@@ -1979,11 +2039,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::
 +
@@ -2623,8 +2688,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
@@ -2806,14 +2870,6 @@
 server. To configure multiple servers the `gerrit.config` file must be edited
 manually.
 
-[[elasticsearch.maxRetryTimeout]]elasticsearch.maxRetryTimeout::
-+
-Sets the maximum timeout to honor in case of multiple retries of the same request.
-+
-The value is in the usual time-unit format like `1 m`, `5 m`.
-+
-Defaults to `30000 ms`.
-
 [[elasticsearch.numberOfShards]]elasticsearch.numberOfShards::
 +
 Sets the number of shards to use per index. Refer to the
@@ -2842,6 +2898,7 @@
 * link:https://www.elastic.co/guide/en/elastic-stack-overview/6.3/security-getting-started.html[Elasticsearch 6.3]
 * link:https://www.elastic.co/guide/en/elastic-stack-overview/6.4/security-getting-started.html[Elasticsearch 6.4]
 * link:https://www.elastic.co/guide/en/elastic-stack-overview/6.5/security-getting-started.html[Elasticsearch 6.5]
+* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.6/security-getting-started.html[Elasticsearch 6.6]
 
 [[elasticsearch.username]]elasticsearch.username::
 +
@@ -3318,9 +3375,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::
 +
@@ -3673,9 +3730,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
@@ -4731,7 +4788,7 @@
 
 +
 The time zone cannot be specified but is always the system default
-time zone.
+time zone. Hours must be zero-padded, i.e. `06:00` rather than `6:00`.
 
 The section (and optionally the subsection) in which the `interval` and
 `startTime` keys must be set depends on the background job for which a
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 8d5dc16..8324a72 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].
@@ -86,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
 
@@ -131,6 +143,18 @@
 rights directly to a single user, since in Gerrit access rights can
 only be assigned to groups.
 
+[[webhooks]]
+=== webhooks
+
+This plugin allows to propagate Gerrit events to remote http endpoints.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/webhooks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[other-plugins]]
 == Other Plugins
 
@@ -222,6 +246,17 @@
 link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[checks]]
+=== checks
+
+The checks plugin provides a REST API and UI extensions for integrating
+CI systems with Gerrit.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/checks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/checks/+doc/master/src/main/resources/Documentation/about.md[
+Plugin Documentation]]
+
 [[egit]]
 === egit
 
@@ -248,6 +283,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
 
@@ -264,13 +310,25 @@
 link:https://gerrit-review.googlesource.com/admin/repos/plugins/github[
 Project]
 
-[[gitiles]]
-=== gitiles
+[[healthcheck]]
+=== healthcheck
 
-Plugin running Gitiles alongside a Gerrit server.
+Plugin for monitoring and alerting when Gerrit does not behave properrly.
 
-link:https://gerrit-review.googlesource.com/admin/repos/plugins/gitiles[
-Project]
+When Gerrit Server needs to be available 24x7, it is important to know
+*beforehand* if something isn't working correctly: this plugin exposes a
+REST-API that provides the real-time status of the Gerrit internals and can
+be integrated with real-time monitoring systems and paging platforms.
+
+Healthcheck metrics (latency and subsystem healthiness) are published as
+Gerrit internal metrics and can be published to dashboards.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/healthcheck[
+Project] |
+link:https://gerrit.googlesource.com/plugins/healthcheck/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/healthcheck/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
 
 [[imagare]]
 === imagare
@@ -526,6 +584,16 @@
 link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[rabbitmq]]
+=== rabbitmq
+
+A plugin that publishes Gerrit events to a
+link:https://www.rabbitmq.com/[RabbitMQ] exchange.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/rabbitmq[Project]
+link:https://gerrit.googlesource.com/plugins/rabbitmq/+/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[readonly]]
 === readonly
 
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 38bfc46..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
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 92596ae..ffd6f01 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -286,33 +286,6 @@
   bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
 ----
 
-The tests run with NoteDb fully enabled by default.
-
-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 //...
-----
-
-NoteDb entirely disabled:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=OFF //...
-----
-
 To run only tests that do not use SSH:
 
 ----
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 6dce1e6..e40af24 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,9 +164,9 @@
 
 To format Java source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.6), and to format Bazel BUILD, WORKSPACE and .bzl files the
+tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
 link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
-tool (version 0.17.2).
+tool (version 0.20.0).
 These tools automatically apply format according to the style guides; this
 streamlines code review by reducing the need for time-consuming, tedious,
 and contentious discussions about trivial issues like whitespace.
@@ -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,10 +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.
+    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).
 
 
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 35073d6..67ced54 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -66,7 +66,7 @@
 
 To format source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.3), which automatically formats code to follow the
+tool (version 1.7), which automatically formats code to follow the
 style guide. See link:dev-contributing.html#style[Code Style] for the
 instruction how to set up command line tool that uses this formatter.
 The Eclipse plugin is provided that allows to format with the same
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 5ec6ae8..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: Tests run with NoteDb enabled by default. If you would like to execute a
-test with NoteDb turned off, add `--test_env=GERRIT_NOTEDB=OFF` 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 e84effd..20c9fa2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -34,10 +34,10 @@
 == Getting started
 
 To get started with the development of a plugin clone the sample
-plugin:
+plugins:
 
 ----
-$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
+$ git clone https://gerrit.googlesource.com/plugins/examples
 ----
 
 This is a project that demonstrates the various features of the
@@ -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]
 ----
@@ -455,7 +455,7 @@
 ----
 import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gwtorm.server.OrmException;
+import com.google.exceptions.StorageException;
 import com.google.inject.Inject;
 
 class MyPlugin {
@@ -469,7 +469,7 @@
   private void postEvent(MyPluginEvent event) {
     try {
       eventDispatcher.get().postEvent(event);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       // error handling
     }
   }
@@ -718,10 +718,9 @@
 
 [source,java]
 ----
-@Singleton
 public class SampleOperator
     implements ChangeQueryBuilder.ChangeOperatorFactory {
-  public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
+  public static class MyPredicate extends PostFilterPredicate<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
@@ -860,55 +858,53 @@
 ----
 
 [[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;
@@ -921,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);
@@ -951,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]]
@@ -1573,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) {
@@ -1662,7 +1674,7 @@
 
 [source,java]
 ----
-public class HttpModule extends HttpPluginModule {
+public class HttpModule extends ServletModule {
   @Override
   protected void configureServlets() {
     DynamicSet.bind(binder(), WebUiPlugin.class)
@@ -2206,9 +2218,9 @@
 /** Register the LfsApiServlet to listen on the default LFS protocol endpoint */
 import static com.google.gerrit.httpd.plugins.LfsPluginServlet.URL_REGEX;
 
-import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.inject.servlet.ServletModule;
 
-public class HttpModule extends HttpPluginModule {
+public class HttpModule extends ServletModule {
 
   @Override
   protected void configureServlets() {
@@ -2533,8 +2545,8 @@
 }
 ----
 
-[[ssh-command-interception]]
-== SSH Command Interception
+[[ssh-command-creation-interception]]
+== SSH Command Creation Interception
 
 Gerrit provides an extension point that allows a plugin to intercept
 creation of SSH commands and override the functionality with its own
@@ -2550,6 +2562,39 @@
     return pluginName + " mycommand";
 ----
 
+[[ssh-command-execution-interception]]
+== SSH Command Execution Interception
+Gerrit provides an extension point that enables plugins to check and
+prevent an SSH command from being run.
+
+[source, java]
+----
+import com.google.gerrit.sshd.SshExecuteCommandInterceptor;
+
+@Singleton
+public class SshExecuteCommandInterceptorImpl implements SshExecuteCommandInterceptor {
+  private final Provider<SshSession> sessionProvider;
+
+  @Inject
+  SshExecuteCommandInterceptorImpl(Provider<SshSession> sessionProvider) {
+    this.sessionProvider = sessionProvider;
+  }
+
+  @Override
+  public boolean accept(String command, List<String> arguments) {
+    if (command.startsWith("gerrit") && !"10.1.2.3".equals(sessionProvider.get().getRemoteAddressAsString())) {
+      return false;
+    }
+    return true;
+  }
+}
+----
+
+And then declare it in your SSH module:
+[source, java]
+----
+  DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class).to(SshExecuteCommandInterceptorImpl.class);
+----
 
 [[pre-submit-evaluator]]
 == Pre-submit Validation Plugins
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 433e0c1..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
diff --git a/Documentation/error-changeid-above-footer.txt b/Documentation/error-changeid-above-footer.txt
new file mode 100644
index 0000000..abc0186
--- /dev/null
+++ b/Documentation/error-changeid-above-footer.txt
@@ -0,0 +1,31 @@
+= commit xxxxxxx: Change-Id must be in message footer
+
+With this error message, Gerrit rejects a push of a commit to a project
+if the commit message of the pushed commit contains a Change-Id line that
+is not in the footer (the last paragraph).
+
+To be picked up by Gerrit, a Change-Id must be in the last paragraph
+of a commit message. For details, see link:user-changeid.html[Change-Id Lines].
+
+You can see the commit messages for existing commits in the history
+by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
+
+
+== Change-Id is contained in the commit message but not in the last paragraph
+
+If the Change-Id is contained in the commit message but not in its
+last paragraph, you have to update the commit message and move the
+Change-Id into the last paragraph. How to update the commit message
+is explained link:error-push-fails-due-to-commit-message.html[here].
+
+To avoid confusion due to a Change-Id that was meant to be picked up by
+Gerrit not being picked up, this is an error whether or not the project
+is configured to always require a Change-Id in the commit message.
+
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
+
+SEARCHBOX
+---------
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index 37eb1f6..b523663 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -18,6 +18,7 @@
 * link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message footer]
 * link:error-invalid-committer.html[invalid committer]
 * link:error-missing-changeid.html[missing Change-Id in commit message footer]
+* link:error-changeid-above-footer.html[Change-Id must be in commit message footer]
 * link:error-missing-subject.html[missing subject; Change-Id must be in commit message footer]
 * link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message footer]
 * link:error-no-common-ancestry.html[no common ancestry]
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index 08f2c09..27bfea5 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -3,13 +3,7 @@
 With this error message Gerrit rejects to push a commit to a project
 which is configured to always require a Change-Id in the commit
 message if the commit message of the pushed commit does not contain
-a Change-Id in the footer (the last paragraph).
-
-This error may happen for different reasons:
-
-. missing Change-Id in the commit message
-. Change-Id is contained in the commit message but not in the last
-  paragraph
+a Change-Id.
 
 You can see the commit messages for existing commits in the history
 by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
@@ -38,17 +32,6 @@
 is explained link:error-push-fails-due-to-commit-message.html[here].
 
 
-== Change-Id is contained in the commit message but not in the last paragraph
-
-To be picked up by Gerrit, a Change-Id must be in the last paragraph
-of a commit message, for details, see link:user-changeid.html[Change-Id Lines].
-
-If the Change-Id is contained in the commit message but not in its
-last paragraph you have to update the commit message and move the
-Change-Id into the last paragraph. How to update the commit message
-is explained link:error-push-fails-due-to-commit-message.html[here].
-
-
 GERRIT
 ------
 Part of link:error-messages.html[Gerrit Error Messages]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index be55417..0885da1 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -61,8 +61,7 @@
 
 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
@@ -87,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
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-rockstar.txt b/Documentation/intro-rockstar.txt
index b60a91f..0b67950 100644
--- a/Documentation/intro-rockstar.txt
+++ b/Documentation/intro-rockstar.txt
@@ -60,7 +60,7 @@
 At least two well-known open source projects insist on these practices:
 
 * link:http://git-scm.com/[Git]
-* link:http://www.kernel.org/category/about.html/[Linux Kernel]
+* link:http://www.kernel.org/category/about.html[Linux Kernel]
 
 However, contributors to these projects don’t refine and polish their changes
 in private until they’re perfect. Instead, polishing code is part of a review
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 4e304f1..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]]
@@ -565,6 +566,11 @@
 View Private Changes] global capability. Private changes are useful in a number
 of cases:
 
+* You want a set of collaborators to review the change before formal review
+  starts. By creating a Private change and adding only a selected few as
+  reviewers you can control who can see the change and get a first opinion
+  before opening up for all reviewers.
+
 * You want to check what the change looks like before formal review starts.
   By marking the change private without reviewers, nobody can
   prematurely comment on your changes.
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 96b5107..4ef2a6c 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -188,6 +188,12 @@
   comments, file-level comments and summary comments, and it may change
   with new Gerrit versions.
 
+* `highlightjs-loaded`: Invoked when the highlight.js library has
+  finished loading. The global `hljs` object (also now accessible via
+  `window.hljs`) is passed as an argument to the callback function.
+  This event can be used to register a new language highlighter with
+  the highlight.js library before syntax highlighting begins.
+
 [[self_onAction]]
 === self.onAction()
 Register a JavaScript callback to be invoked when the user clicks
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
new file mode 100644
index 0000000..6a83980
--- /dev/null
+++ b/Documentation/js_licenses.txt
@@ -0,0 +1,509 @@
+
+[[Apache2_0]]
+Apache2.0
+
+* fonts:robotofonts
+* js:web-animations-js
+* polymer_externs:polymer_closure
+
+[[Apache2_0_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* js:ba-linkify
+
+[[ba-linkify_license]]
+----
+Copyright (c) 2009 "Cowboy" Ben Alman
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[es6-promise]]
+es6-promise
+
+* js:es6-promise
+
+[[es6-promise_license]]
+----
+Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[fetch]]
+fetch
+
+* js:fetch
+
+[[fetch_license]]
+----
+Copyright (c) 2014-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[highlightjs]]
+highlightjs
+
+* js:highlightjs
+* js:highlightjs_files
+
+[[highlightjs_license]]
+----
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of highlight.js nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+----
+
+
+[[moment]]
+moment
+
+* js:moment
+
+[[moment_license]]
+----
+Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[page_js]]
+page.js
+
+* js:page
+
+[[page_js_license]]
+----
+(The MIT License)
+
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[polymer]]
+polymer
+
+* js:font-roboto
+* js:iron-a11y-announcer
+* js:iron-a11y-keys-behavior
+* js:iron-autogrow-textarea
+* js:iron-behaviors
+* js:iron-checked-element-behavior
+* js:iron-dropdown
+* js:iron-fit-behavior
+* js:iron-flex-layout
+* js:iron-form-element-behavior
+* js:iron-icon
+* js:iron-iconset-svg
+* js:iron-input
+* js:iron-menu-behavior
+* js:iron-meta
+* js:iron-overlay-behavior
+* js:iron-resizable-behavior
+* js:iron-selector
+* js:iron-validatable-behavior
+* js:neon-animation
+* js:paper-behaviors
+* js:paper-button
+* js:paper-icon-button
+* js:paper-input
+* js:paper-item
+* js:paper-listbox
+* js:paper-ripple
+* js:paper-styles
+* js:paper-tabs
+* js:paper-toggle-button
+* js:polymer
+* js:polymer-resin
+* js:webcomponentsjs
+
+[[polymer_license]]
+----
+Copyright (c) 2014 The Polymer Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[promise-polyfill]]
+promise-polyfill
+
+* js:promise-polyfill
+
+[[promise-polyfill_license]]
+----
+Copyright (c) 2014 Taylor Hakes
+Copyright (c) 2014 Forbes Lindesay
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+----
+
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
new file mode 100644
index 0000000..6e61e29
--- /dev/null
+++ b/Documentation/licenses.txt
@@ -0,0 +1,3468 @@
+= Gerrit Code Review - Licenses
+
+// DO NOT EDIT - GENERATED AUTOMATICALLY.
+
+Gerrit open source software is licensed under the <<Apache2_0,Apache
+License 2.0>>.  Executable distributions also include other software
+components that are provided under additional licenses.
+
+[[cryptography]]
+== Cryptography Notice
+
+This distribution includes cryptographic software.  The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software.  BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted.  See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms.  The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+== Licenses
+
+
+[[Apache2_0]]
+Apache2.0
+
+* auto:auto-value
+* auto:auto-value-annotations
+* commons:codec
+* commons:compress
+* commons:dbcp
+* commons:lang
+* commons:net
+* commons:pool
+* commons:validator
+* dropwizard:dropwizard-core
+* flogger:api
+* fonts:robotofonts
+* guice:guice
+* guice:guice-assistedinject
+* guice:guice-library
+* guice:guice-servlet
+* guice:javax_inject
+* httpcomponents:httpasyncclient
+* httpcomponents:httpclient
+* httpcomponents:httpcore
+* httpcomponents:httpcore-nio
+* jackson:jackson-core
+* jetty:continuation
+* jetty:http
+* jetty:io
+* jetty:jmx
+* jetty:security
+* jetty:server
+* jetty:servlet
+* jetty:util
+* jgit/org.eclipse.jgit:javaewah
+* js:web-animations-js
+* log:json-smart
+* log:jsonevent-layout
+* log:log4j
+* lucene:lucene-analyzers-common
+* lucene:lucene-core-and-backward-codecs
+* lucene:lucene-misc
+* lucene:lucene-queryparser
+* mime4j:core
+* mime4j:dom
+* mina:core
+* mina:sshd
+* openid:consumer
+* openid:nekohtml
+* openid:xerces
+* polymer_externs:polymer_closure
+* blame-cache
+* gson
+* guava
+* guava-failureaccess
+* guava-retrying
+* html-types
+* j2objc
+* jsr305
+* mime-util
+* servlet-api-3_1
+* servlet-api-3_1-without-neverlink
+* soy
+
+[[Apache2_0_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
+[[CC0-1_0]]
+CC0-1.0
+
+* mina:eddsa
+
+[[CC0-1_0_license]]
+----
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
+
+For more information, please see https://creativecommons.org/publicdomain/zero/1.0/
+
+----
+
+
+[[MPL1_1]]
+MPL1.1
+
+* juniversalchardet
+
+[[MPL1_1_license]]
+----
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+EXHIBIT A -Mozilla Public License.
+
+     ``The contents of this file are subject to the Mozilla Public License
+     Version 1.1 (the "License"); you may not use this file except in
+     compliance with the License. You may obtain a copy of the License at
+     http://www.mozilla.org/MPL/
+
+     Software distributed under the License is distributed on an "AS IS"
+     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+     License for the specific language governing rights and limitations
+     under the License.
+
+     The Original Code is ______________________________________.
+
+     The Initial Developer of the Original Code is ________________________.
+     Portions created by ______________________ are Copyright (C) ______
+     _______________________. All Rights Reserved.
+
+     Contributor(s): ______________________________________.
+
+     Alternatively, the contents of this file may be used under the terms
+     of the _____ license (the  "[___] License"), in which case the
+     provisions of [______] License are applicable instead of those
+     above.  If you wish to allow use of your version of this file only
+     under the terms of the [____] License and not to allow others to use
+     your version of this file under the MPL, indicate your decision by
+     deleting  the provisions above and replace  them with the notice and
+     other provisions required by the [___] License.  If you do not delete
+     the provisions above, a recipient may use your version of this file
+     under either the MPL or the [___] License."
+
+     [NOTE: The text of this Exhibit A may differ slightly from the text of
+     the notices in the Source Code files of the Original Code. You should
+     use the text of this Exhibit A rather than the text found in the
+     Original Code Source Code for Your Modifications.]
+
+----
+
+
+[[PublicDomain]]
+PublicDomain
+
+* guice:aopalliance
+
+[[PublicDomain_license]]
+----
+This software has been placed in the public domain by its author(s).
+
+----
+
+
+[[antlr]]
+antlr
+
+* antlr:antlr27
+* antlr:java-runtime
+* antlr:stringtemplate
+* antlr:tool
+
+[[antlr_license]]
+----
+Copyright (c) 2003-2008, Terence Parr
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[args4j]]
+args4j
+
+* args4j
+
+[[args4j_license]]
+----
+Copyright (c) 2013 Kohsuke Kawaguchi and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[autolink]]
+autolink
+
+* autolink
+
+[[autolink_license]]
+----
+The MIT License (MIT)
+
+Copyright (c) 2015 Robin Stocker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[automaton]]
+automaton
+
+* automaton
+
+[[automaton_license]]
+----
+Copyright (c) 2001-2011 Anders Moeller
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* js:ba-linkify
+
+[[ba-linkify_license]]
+----
+Copyright (c) 2009 "Cowboy" Ben Alman
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[bouncycastle]]
+bouncycastle
+
+* bouncycastle:bcpg-neverlink
+* bouncycastle:bcpkix-neverlink
+* bouncycastle:bcprov-neverlink
+
+[[bouncycastle_license]]
+----
+Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc.
+(http://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[elasticsearch]]
+elasticsearch
+
+* elasticsearch-rest-client:elasticsearch-rest-client
+
+[[elasticsearch_license]]
+----
+Elasticsearch
+Copyright 2009-2015 Elasticsearch
+
+This product includes software developed by The Apache Software
+Foundation (http://www.apache.org/).
+
+----
+
+
+[[es6-promise]]
+es6-promise
+
+* js:es6-promise
+
+[[es6-promise_license]]
+----
+Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+
+[[fetch]]
+fetch
+
+* js:fetch
+
+[[fetch_license]]
+----
+Copyright (c) 2014-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[flexmark]]
+flexmark
+
+* flexmark
+* flexmark-ext-abbreviation
+* flexmark-ext-anchorlink
+* flexmark-ext-autolink
+* flexmark-ext-definition
+* flexmark-ext-emoji
+* flexmark-ext-escaped-character
+* flexmark-ext-footnotes
+* flexmark-ext-gfm-issues
+* flexmark-ext-gfm-strikethrough
+* flexmark-ext-gfm-tables
+* flexmark-ext-gfm-tasklist
+* flexmark-ext-gfm-users
+* flexmark-ext-ins
+* flexmark-ext-jekyll-front-matter
+* flexmark-ext-superscript
+* flexmark-ext-tables
+* flexmark-ext-toc
+* flexmark-ext-typographic
+* flexmark-ext-wikilink
+* flexmark-ext-yaml-front-matter
+* flexmark-formatter
+* flexmark-html-parser
+* flexmark-profile-pegdown
+* flexmark-util
+
+[[flexmark_license]]
+----
+Copyright (c) 2015-2016, Atlassian Pty Ltd
+All rights reserved.
+
+Copyright (c) 2016, Vladimir Schneider,
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[h2]]
+h2
+
+* h2
+
+[[h2_license]]
+----
+H2 is dual licensed and available under a modified version of the
+MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0.
+----
+
+link:http://www.h2database.com/html/license.html[H2 License]
+
+----
+H2 License - Version 1.0
+1. Definitions
+
+1.0.1. "Commercial Use" means distribution or otherwise making the
+       Covered Code available to a third party.
+
+1.1. "Contributor" means each entity that creates or contributes
+     to the creation of Modifications.
+
+1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the
+     Modifications made by that particular Contributor.
+
+1.3. "Covered Code" means the Original Code or Modifications or
+     the combination of the Original Code and Modifications, in each
+     case including portions thereof.
+
+1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+1.5. "Executable" means Covered Code in any form other than Source Code.
+
+1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required
+     by Exhibit A.
+
+1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this
+     License.
+
+1.8. "License" means this document.
+
+1.8.1. "Licensable" means having the right to grant, to the maximum
+       extent possible, whether at the time of the initial grant
+       or subsequently acquired, any and all of the rights conveyed
+       herein.
+
+1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any
+     previous Modifications. When Covered Code is released as a
+     series of files, a Modification is:
+
+1.9.a. Any addition to or deletion from the contents of a file
+       containing Original Code or previous Modifications.
+
+1.9.b. Any new file that contains any part of the Original Code or
+       previous Modifications.
+
+1.10. "Original Code" means Source Code of computer software
+      code which is described in the Source Code notice required
+      by Exhibit A as Original Code, and which, at the time of
+      its release under this License is not already Covered Code
+      governed by this License.
+
+1.10.1. "Patent Claims" means any patent claim(s), now owned or
+        hereafter acquired, including without limitation, method,
+        process, and apparatus claims, in any patent Licensable
+        by grantor.
+
+1.11. "Source Code" means the preferred form of the Covered Code
+      for making modifications to it, including all modules it
+      contains, plus any associated interface definition files,
+      scripts used to control compilation and installation of an
+      Executable, or source code differential comparisons against
+      either the Original Code or another well known, available
+      Covered Code of the Contributor's choice. The Source Code can
+      be in a compressed or archival form, provided the appropriate
+      decompression or de-archiving software is widely available
+      for no charge.
+
+1.12. "You" (or "Your") means an individual or a legal entity
+      exercising rights under, and complying with all of the terms
+      of, this License or a future version of this License issued
+      under Section 6.1. For legal entities, "You" includes any
+      entity which controls, is controlled by, or is under common
+      control with You. For purposes of this definition, "control"
+      means (a) the power, direct or indirect, to cause the direction
+      or management of such entity, whether by contract or otherwise,
+      or (b) ownership of more than fifty percent (50%) of the
+      outstanding shares or beneficial ownership of such entity.
+
+2. Source Code License
+
+2.1. The Initial Developer Grant
+
+The Initial Developer hereby grants You a world-wide, royalty-free,
+non-exclusive license, subject to third party intellectual property
+claims:
+
+2.1.a. under intellectual property rights (other than patent
+       or trademark) Licensable by Initial Developer to use,
+       reproduce, modify, display, perform, sublicense and distribute
+       the Original Code (or portions thereof) with or without
+       Modifications, and/or as part of a Larger Work; and
+
+2.1.b. under Patents Claims infringed by the making, using or selling
+       of Original Code, to make, have made, use, practice, sell,
+       and offer for sale, and/or otherwise dispose of the Original
+       Code (or portions thereof).
+
+2.1.c. the licenses granted in this Section 2.1 (a) and (b) are
+       effective on the date Initial Developer first distributes
+       Original Code under the terms of this License.
+
+2.1.d. Notwithstanding Section 2.1 (b) above, no patent license is
+       granted: 1) for code that You delete from the Original Code;
+       2) separate from the Original Code; or 3) for infringements
+       caused by: i) the modification of the Original Code or ii)
+       the combination of the Original Code with other software
+       or devices.
+
+2.2. Contributor Grant
+
+Subject to third party intellectual property claims, each Contributor
+hereby grants You a world-wide, royalty-free, non-exclusive license
+
+2.2.a. under intellectual property rights (other than patent or
+       trademark) Licensable by Contributor, to use, reproduce,
+       modify, display, perform, sublicense and distribute the
+       Modifications created by such Contributor (or portions
+       thereof) either on an unmodified basis, with other
+       Modifications, as Covered Code and/or as part of a Larger
+       Work; and
+
+2.2.b. under Patent Claims infringed by the making, using, or selling
+       of Modifications made by that Contributor either alone and/or
+       in combination with its Contributor Version (or portions
+       of such combination), to make, use, sell, offer for sale,
+       have made, and/or otherwise dispose of: 1) Modifications
+       made by that Contributor (or portions thereof); and 2) the
+       combination of Modifications made by that Contributor with
+       its Contributor Version (or portions of such combination).
+
+2.2.c. the licenses granted in Sections 2.2 (a) and 2.2 (b) are
+       effective on the date Contributor first makes Commercial
+       Use of the Covered Code.
+
+2.2.c. Notwithstanding Section 2.2 (b) above, no patent license is
+       granted: 1) for any code that Contributor has deleted from
+       the Contributor Version; 2) separate from the Contributor
+       Version; 3) for infringements caused by: i) third party
+       modifications of Contributor Version or ii) the combination
+       of Modifications made by that Contributor with other software
+       (except as part of the Contributor Version) or other devices;
+       or 4) under Patent Claims infringed by Covered Code in the
+       absence of Modifications made by that Contributor.
+
+3. Distribution Obligations
+
+3.1. Application of License
+
+The Modifications which You create or to which You contribute
+are governed by the terms of this License, including without
+limitation Section 2.2. The Source Code version of Covered Code may
+be distributed only under the terms of this License or a future
+version of this License released under Section 6.1, and You must
+include a copy of this License with every copy of the Source Code
+You distribute. You may not offer or impose any terms on any Source
+Code version that alters or restricts the applicable version of
+this License or the recipients' rights hereunder. However, You
+may include an additional document offering the additional rights
+described in Section 3.5.
+
+3.2. Availability of Source Code
+
+Any Modification which You create or to which You contribute must
+be made available in Source Code form under the terms of this
+License either on the same media as an Executable version or via
+an accepted Electronic Distribution Mechanism to anyone to whom
+you made an Executable version available; and if made available
+via Electronic Distribution Mechanism, must remain available for
+at least twelve (12) months after the date it initially became
+available, or at least six (6) months after a subsequent version
+of that particular Modification has been made available to such
+recipients. You are responsible for ensuring that the Source Code
+version remains available even if the Electronic Distribution
+Mechanism is maintained by a third party.
+
+3.3. Description of Modifications
+
+You must cause all Covered Code to which You contribute to contain
+a file documenting the changes You made to create that Covered
+Code and the date of any change. You must include a prominent
+statement that the Modification is derived, directly or indirectly,
+from Original Code provided by the Initial Developer and including
+the name of the Initial Developer in (a) the Source Code, and (b)
+in any notice in an Executable version or related documentation in
+which You describe the origin or ownership of the Covered Code.
+
+3.4. Intellectual Property Matters
+
+3.4.a. Third Party Claims: If Contributor has knowledge that
+       a license under a third party's intellectual property
+       rights is required to exercise the rights granted by such
+       Contributor under Sections 2.1 or 2.2, Contributor must
+       include a text file with the Source Code distribution titled
+       "LEGAL" which describes the claim and the party making the
+       claim in sufficient detail that a recipient will know whom
+       to contact. If Contributor obtains such knowledge after the
+       Modification is made available as described in Section 3.2,
+       Contributor shall promptly modify the LEGAL file in all
+       copies Contributor makes available thereafter and shall take
+       other steps (such as notifying appropriate mailing lists or
+       newsgroups) reasonably calculated to inform those who received
+       the Covered Code that new knowledge has been obtained.
+
+3.4.b. Contributor APIs: If Contributor's Modifications include
+       an application programming interface and Contributor has
+       knowledge of patent licenses which are reasonably necessary
+       to implement that API, Contributor must also include this
+       information in the legal file.
+
+3.4.c. Representations: Contributor represents that, except as
+       disclosed pursuant to Section 3.4 (a) above, Contributor
+       believes that Contributor's Modifications are Contributor's
+       original creation(s) and/or Contributor has sufficient rights
+       to grant the rights conveyed by this License.
+
+3.5. Required Notices
+
+You must duplicate the notice in Exhibit A in each file of
+the Source Code. If it is not possible to put such notice in a
+particular Source Code file due to its structure, then You must
+include such notice in a location (such as a relevant directory)
+where a user would be likely to look for such a notice. If You
+created one or more Modification(s) You may add your name as a
+Contributor to the notice described in Exhibit A. You must also
+duplicate this License in any documentation for the Source Code
+where You describe recipients' rights or ownership rights relating
+to Covered Code. You may choose to offer, and to charge a fee for,
+warranty, support, indemnity or liability obligations to one or
+more recipients of Covered Code. However, You may do so only on
+Your own behalf, and not on behalf of the Initial Developer or
+any Contributor. You must make it absolutely clear than any such
+warranty, support, indemnity or liability obligation is offered by
+You alone, and You hereby agree to indemnify the Initial Developer
+and every Contributor for any liability incurred by the Initial
+Developer or such Contributor as a result of warranty, support,
+indemnity or liability terms You offer.
+
+3.6. Distribution of Executable Versions
+
+You may distribute Covered Code in Executable form only if the
+requirements of Sections 3.1, 3.2, 3.3, 3.4 and 3.5 have been met
+for that Covered Code, and if You include a notice stating that
+the Source Code version of the Covered Code is available under the
+terms of this License, including a description of how and where
+You have fulfilled the obligations of Section 3.2. The notice
+must be conspicuously included in any notice in an Executable
+version, related documentation or collateral in which You describe
+recipients' rights relating to the Covered Code. You may distribute
+the Executable version of Covered Code or ownership rights under
+a license of Your choice, which may contain terms different from
+this License, provided that You are in compliance with the terms
+of this License and that the license for the Executable version
+does not attempt to limit or alter the recipient's rights in the
+Source Code version from the rights set forth in this License. If
+You distribute the Executable version under a different license You
+must make it absolutely clear that any terms which differ from this
+License are offered by You alone, not by the Initial Developer or any
+Contributor. You hereby agree to indemnify the Initial Developer and
+every Contributor for any liability incurred by the Initial Developer
+or such Contributor as a result of any such terms You offer.
+
+3.7. Larger Works
+
+You may create a Larger Work by combining Covered Code with other
+code not governed by the terms of this License and distribute the
+Larger Work as a single product. In such a case, You must make sure
+the requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+If it is impossible for You to comply with any of the terms of
+this License with respect to some or all of the Covered Code due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description
+must be included in the legal file described in Section 3.4 and
+must be included with all distributions of the Source Code. Except
+to the extent prohibited by statute or regulation, such description
+must be sufficiently detailed for a recipient of ordinary skill to
+be able to understand it.
+
+5. Application of this License.
+
+This License applies to code to which the Initial Developer has
+attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+6.1. New Versions
+
+The H2 Group may publish revised and/or new versions of the License
+from time to time. Each version will be given a distinguishing
+version number.
+
+6.2. Effect of New Versions
+
+Once Covered Code has been published under a particular version of
+the License, You may always continue to use it under the terms of
+that version. You may also choose to use such Covered Code under the
+terms of any subsequent version of the License published by the H2
+Group. No one other than the H2 Group has the right to modify the
+terms applicable to Covered Code created under this License.
+
+6.3. Derivative Works
+
+If You create or use a modified version of this License (which you
+may only do in order to apply it to code which is not already Covered
+Code governed by this License), You must (a) rename Your license so
+that the phrases "H2 Group", "H2" or any confusingly similar phrase
+do not appear in your license (except to note that your license
+differs from this License) and (b) otherwise make it clear that
+Your version of the license contains terms which differ from the
+H2 License. (Filling in the name of the Initial Developer, Original
+Code or Contributor in the notice described in Exhibit A shall not
+of themselves be deemed to be modifications of this License.)
+
+7. Disclaimer of Warranty
+
+Covered code is provided under this license on an "as is" basis,
+without warranty of any kind, either expressed or implied,
+including, without limitation, warranties that the covered code
+is free of defects, merchantable, fit for a particular purpose or
+non-infringing. The entire risk as to the quality and performance
+of the covered code is with you. Should any covered code prove
+defective in any respect, you (not the initial developer or any
+other contributor) assume the cost of any necessary servicing,
+repair or correction. This disclaimer of warranty constitutes
+an essential part of this license. No use of any covered code is
+authorized hereunder except under this disclaimer.
+
+8. Termination
+
+8.1. This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and
+     fail to cure such breach within 30 days of becoming aware
+     of the breach. All sublicenses to the Covered Code which
+     are properly granted shall survive any termination of this
+     License. Provisions which, by their nature, must remain in
+     effect beyond the termination of this License shall survive.
+
+8.2. If You initiate litigation by asserting a patent infringement
+     claim (excluding declaratory judgment actions) against
+     Initial Developer or a Contributor (the Initial Developer or
+     Contributor against whom You file such action is referred to as
+     "Participant") alleging that:
+
+8.2.a. such Participant's Contributor Version directly or indirectly
+       infringes any patent, then any and all rights granted by
+       such Participant to You under Sections 2.1 and/or 2.2 of this
+       License shall, upon 60 days notice from Participant terminate
+       prospectively, unless if within 60 days after receipt of
+       notice You either: (i) agree in writing to pay Participant
+       a mutually agreeable reasonable royalty for Your past and
+       future use of Modifications made by such Participant, or (ii)
+       withdraw Your litigation claim with respect to the Contributor
+       Version against such Participant. If within 60 days of notice,
+       a reasonable royalty and payment arrangement are not mutually
+       agreed upon in writing by the parties or the litigation claim
+       is not withdrawn, the rights granted by Participant to You
+       under Sections 2.1 and/or 2.2 automatically terminate at
+       the expiration of the 60 day notice period specified above.
+
+8.2.b. any software, hardware, or device, other than such
+       Participant's Contributor Version, directly or indirectly
+       infringes any patent, then any rights granted to You by
+       such Participant under Sections 2.1(b) and 2.2(b) are
+       revoked effective as of the date You first made, used,
+       sold, distributed, or had made, Modifications made by that
+       Participant.
+
+8.3. If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly
+     or indirectly infringes any patent where such claim is resolved
+     (such as by license or settlement) prior to the initiation of
+     patent infringement litigation, then the reasonable value of
+     the licenses granted by such Participant under Sections 2.1
+     or 2.2 shall be taken into account in determining the amount
+     or value of any payment or license.
+
+8.4. In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and
+     resellers) which have been validly granted by You or any
+     distributor hereunder prior to termination shall survive
+     termination.
+
+9. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort
+(including negligence), contract, or otherwise, shall you, the
+initial developer, any other contributor, or any distributor of
+covered code, or any supplier of any of such parties, be liable to
+any person for any indirect, special, incidental, or consequential
+damages of any character including, without limitation, damages for
+loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses, even if such party
+shall have been informed of the possibility of such damages. This
+limitation of liability shall not apply to liability for death or
+personal injury resulting from such party's negligence to the extent
+applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential
+damages, so this exclusion and limitation may not apply to you.
+
+10. United States Government End Users
+
+The Covered Code is a "commercial item", as that term is defined in
+48 C.F.R. 2.101 (October 1995), consisting of "commercial computer
+software" and "commercial computer software documentation", as such
+terms are used in 48 C.F.R. 12.212 (September 1995). Consistent
+with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4
+(June 1995), all U.S. Government End Users acquire Covered Code
+with only those rights set forth herein.
+
+11. Miscellaneous
+
+This License represents the complete agreement concerning subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. This License shall be governed
+by California law provisions (except to the extent applicable
+law, if any, provides otherwise), excluding its conflict-of-law
+provisions. With respect to disputes in which at least one party is
+a citizen of, or an entity chartered or registered to do business in
+United States of America, any litigation relating to this License
+shall be subject to the jurisdiction of the Federal Courts of the
+Northern District of California, with venue lying in Santa Clara
+County, California, with the losing party responsible for costs,
+including without limitation, court costs and reasonable attorneys'
+fees and expenses. The application of the United Nations Convention
+on Contracts for the International Sale of Goods is expressly
+excluded. Any law or regulation which provides that the language of
+a contract shall be construed against the drafter shall not apply
+to this License.
+
+12. Responsibility for Claims
+
+As between Initial Developer and the Contributors, each party is
+responsible for claims and damages arising, directly or indirectly,
+out of its utilization of rights under this License and You agree
+to work with Initial Developer and Contributors to distribute such
+responsibility on an equitable basis. Nothing herein is intended
+or shall be deemed to constitute any admission of liability.
+
+13. Multiple-Licensed Code
+
+Initial Developer may designate portions of the Covered Code as
+"Multiple-Licensed". "Multiple-Licensed" means that the Initial
+Developer permits you to utilize portions of the Covered Code under
+Your choice of this or the alternative licenses, if any, specified
+by the Initial Developer in the file described in Exhibit A.
+
+Exhibit A
+
+Multiple-Licensed under the H2 License, Version 1.0,
+and under the Eclipse Public License, Version 1.0
+(http://h2database.com/html/license.html).
+Initial Developer: H2 Group
+----
+
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+----
+
+----
+Export Control Classification Number (ECCN)
+
+As far as we know, the U.S. Export Control Classification Number
+(ECCN) for this software is 5D002. However, for legal reasons, we
+can make no warranty that this information is correct. For details,
+see also the Apache Software Foundation Export Classifications page.
+
+----
+
+
+[[highlightjs]]
+highlightjs
+
+* js:highlightjs
+* js:highlightjs_files
+
+[[highlightjs_license]]
+----
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of highlight.js nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+----
+
+
+[[icu4j]]
+icu4j
+
+* icu4j
+
+[[icu4j_license]]
+----
+COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
+
+Copyright © 1991-2016 Unicode, Inc. All rights reserved.
+Distributed under the Terms of Use in http://www.unicode.org/copyright.html
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation
+(the "Data Files") or Unicode software and any associated documentation
+(the "Software") to deal in the Data Files or Software
+without restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, and/or sell copies of
+the Data Files or Software, and to permit persons to whom the Data Files
+or Software are furnished to do so, provided that either
+(a) this copyright and permission notice appear with all copies
+of the Data Files or Software, or
+(b) this copyright and permission notice appear in associated
+Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT OF THIRD PARTY RIGHTS.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
+NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale,
+use or other dealings in these Data Files or Software without prior
+written authorization of the copyright holder.
+
+---------------------
+
+Third-Party Software Licenses
+
+This section contains third-party software notices and/or additional
+terms for licensed third-party software components included within ICU
+libraries.
+
+1. ICU License - ICU 1.8.1 to ICU 57.1
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2016 International Business Machines Corporation and others
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies of
+the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
+SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in this Software without prior written authorization
+of the copyright holder.
+
+All trademarks and registered trademarks mentioned herein are the
+property of their respective owners.
+
+2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
+
+ #     The Google Chrome software developed by Google is licensed under
+ # the BSD license. Other software included in this distribution is
+ # provided under other licenses, as set forth below.
+ #
+ #  The BSD License
+ #  http://opensource.org/licenses/bsd-license.php
+ #  Copyright (C) 2006-2008, Google Inc.
+ #
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ # modification, are permitted provided that the following conditions are met:
+ #
+ #  Redistributions of source code must retain the above copyright notice,
+ # this list of conditions and the following disclaimer.
+ #  Redistributions in binary form must reproduce the above
+ # copyright notice, this list of conditions and the following
+ # disclaimer in the documentation and/or other materials provided with
+ # the distribution.
+ #  Neither the name of  Google Inc. nor the names of its
+ # contributors may be used to endorse or promote products derived from
+ # this software without specific prior written permission.
+ #
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #
+ #
+ #  The word list in cjdict.txt are generated by combining three word lists
+ # listed below with further processing for compound word breaking. The
+ # frequency is generated with an iterative training against Google web
+ # corpora.
+ #
+ #  * Libtabe (Chinese)
+ #    - https://sourceforge.net/project/?group_id=1519
+ #    - Its license terms and conditions are shown below.
+ #
+ #  * IPADIC (Japanese)
+ #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ #    - Its license terms and conditions are shown below.
+ #
+ #  ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ #  /*
+ #   * Copyrighy (c) 1999 TaBE Project.
+ #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ #   * All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the TaBE Project nor the names of its
+ #   *   contributors may be used to endorse or promote products derived
+ #   *   from this software without specific prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  /*
+ #   * Copyright (c) 1999 Computer Systems and Communication Lab,
+ #   *                    Institute of Information Science, Academia
+ #       *                    Sinica. All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the Computer Systems and Communication Lab
+ #   *   nor the names of its contributors may be used to endorse or
+ #   *   promote products derived from this software without specific
+ #   *   prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
+ #      University of Illinois
+ #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ #  ---------------COPYING.libtabe-----END--------------------------------
+ #
+ #
+ #  ---------------COPYING.ipadic-----BEGIN-------------------------------
+ #
+ #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ #  and Technology.  All Rights Reserved.
+ #
+ #  Use, reproduction, and distribution of this software is permitted.
+ #  Any copy of this software, whether in its original form or modified,
+ #  must include both the above copyright notice and the following
+ #  paragraphs.
+ #
+ #  Nara Institute of Science and Technology (NAIST),
+ #  the copyright holders, disclaims all warranties with regard to this
+ #  software, including all implied warranties of merchantability and
+ #  fitness, in no event shall NAIST be liable for
+ #  any special, indirect or consequential damages or any damages
+ #  whatsoever resulting from loss of use, data or profits, whether in an
+ #  action of contract, negligence or other tortuous action, arising out
+ #  of or in connection with the use or performance of this software.
+ #
+ #  A large portion of the dictionary entries
+ #  originate from ICOT Free Software.  The following conditions for ICOT
+ #  Free Software applies to the current dictionary as well.
+ #
+ #  Each User may also freely distribute the Program, whether in its
+ #  original form or modified, to any third party or parties, PROVIDED
+ #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ #  on, or be attached to, the Program, which is distributed substantially
+ #  in the same form as set out herein and that such intended
+ #  distribution, if actually made, will neither violate or otherwise
+ #  contravene any of the laws and regulations of the countries having
+ #  jurisdiction over the User or the intended distribution itself.
+ #
+ #  NO WARRANTY
+ #
+ #  The program was produced on an experimental basis in the course of the
+ #  research and development conducted during the project and is provided
+ #  to users as so produced on an experimental basis.  Accordingly, the
+ #  program is provided without any warranty whatsoever, whether express,
+ #  implied, statutory or otherwise.  The term "warranty" used herein
+ #  includes, but is not limited to, any warranty of the quality,
+ #  performance, merchantability and fitness for a particular purpose of
+ #  the program and the nonexistence of any infringement or violation of
+ #  any right of any third party.
+ #
+ #  Each user of the program will agree and understand, and be deemed to
+ #  have agreed and understood, that there is no warranty whatsoever for
+ #  the program and, accordingly, the entire risk arising from or
+ #  otherwise connected with the program is assumed by the user.
+ #
+ #  Therefore, neither ICOT, the copyright holder, or any other
+ #  organization that participated in or was otherwise related to the
+ #  development of the program and their respective officials, directors,
+ #  officers and other employees shall be held liable for any and all
+ #  damages, including, without limitation, general, special, incidental
+ #  and consequential damages, arising out of or otherwise in connection
+ #  with the use or inability to use the program or any product, material
+ #  or result produced or otherwise obtained by using the program,
+ #  regardless of whether they have been advised of, or otherwise had
+ #  knowledge of, the possibility of such damages at any time during the
+ #  project or thereafter.  Each user will be deemed to have agreed to the
+ #  foregoing by his or her commencement of use of the program.  The term
+ #  "use" as used herein includes, but is not limited to, the use,
+ #  modification, copying and distribution of the program and the
+ #  production of secondary products from the program.
+ #
+ #  In the case where the program, whether in its original form or
+ #  modified, was distributed or delivered to or received by a user from
+ #  any person, organization or entity other than ICOT, unless it makes or
+ #  grants independently of ICOT any specific warranty to the user in
+ #  writing, such person, organization or entity, will also be exempted
+ #  from and not be held liable to the user for any such damages as noted
+ #  above as far as the program is concerned.
+ #
+ #  ---------------COPYING.ipadic-----END----------------------------------
+
+3. Lao Word Break Dictionary Data (laodict.txt)
+
+ #  Copyright (c) 2013 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ # Project: http://code.google.com/p/lao-dictionary/
+ # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
+ # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
+ #              (copied below)
+ #
+ #  This file is derived from the above dictionary, with slight
+ #  modifications.
+ #  ----------------------------------------------------------------------
+ #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification,
+ #  are permitted provided that the following conditions are met:
+ #
+ #
+ # Redistributions of source code must retain the above copyright notice, this
+ #  list of conditions and the following disclaimer. Redistributions in
+ #  binary form must reproduce the above copyright notice, this list of
+ #  conditions and the following disclaimer in the documentation and/or
+ #  other materials provided with the distribution.
+ #
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+4. Burmese Word Break Dictionary Data (burmesedict.txt)
+
+ #  Copyright (c) 2014 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ #  This list is part of a project hosted at:
+ #    github.com/kanyawtech/myanmar-karen-word-lists
+ #
+ #  --------------------------------------------------------------------------
+ #  Copyright (c) 2013, LeRoy Benjamin Sharon
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification, are permitted provided that the following conditions
+ #  are met: Redistributions of source code must retain the above
+ #  copyright notice, this list of conditions and the following
+ #  disclaimer.  Redistributions in binary form must reproduce the
+ #  above copyright notice, this list of conditions and the following
+ #  disclaimer in the documentation and/or other materials provided
+ #  with the distribution.
+ #
+ #    Neither the name Myanmar Karen Word Lists, nor the names of its
+ #    contributors may be used to endorse or promote products derived
+ #    from this software without specific prior written permission.
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ #  SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+5. Time Zone Database
+
+  ICU uses the public domain data and code derived from Time Zone
+Database for its time zone support. The ownership of the TZ database
+is explained in BCP 175: Procedure for Maintaining the Time Zone
+Database section 7.
+
+ # 7.  Database Ownership
+ #
+ #    The TZ database itself is not an IETF Contribution or an IETF
+ #    document.  Rather it is a pre-existing and regularly updated work
+ #    that is in the public domain, and is intended to remain in the
+ #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
+ #    not apply to the TZ Database or contributions that individuals make
+ #    to it.  Should any claims be made and substantiated against the TZ
+ #    Database, the organization that is providing the IANA
+ #    Considerations defined in this RFC, under the memorandum of
+ #    understanding with the IETF, currently ICANN, may act in accordance
+ #    with all competent court orders.  No ownership claims will be made
+ #    by ICANN or the IETF Trust on the database or the code.  Any person
+ #    making a contribution to the database or code waives all rights to
+ #    future claims in that contribution or in the TZ Database.
+
+----
+
+
+[[jgit]]
+jgit
+
+* jgit/org.eclipse.jgit.archive:jgit-archive
+* jgit/org.eclipse.jgit.http.server:jgit-servlet
+* jgit/org.eclipse.jgit:jgit
+
+[[jgit_license]]
+----
+This program and the accompanying materials are made available
+under the terms of the Eclipse Distribution License v1.0 which
+accompanies this distribution, is reproduced below, and is
+available at http://www.eclipse.org/org/documents/edl-v10.php
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided
+  with the distribution.
+
+- Neither the name of the Eclipse Foundation, Inc. nor the
+  names of its contributors may be used to endorse or promote
+  products derived from this software without specific prior
+  written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[jsch]]
+jsch
+
+* jsch
+
+[[jsch_license]]
+----
+Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in
+     the documentation and/or other materials provided with the distribution.
+
+  3. The names of the authors may not be used to endorse or promote products
+     derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[jsoup]]
+jsoup
+
+* jsoup:jsoup
+
+[[jsoup_license]]
+----
+The MIT License
+
+© 2009-2016, Jonathan Hedley <jonathan@hedley.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+----
+
+
+[[moment]]
+moment
+
+* js:moment
+
+[[moment_license]]
+----
+Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[ow2]]
+ow2
+
+* ow2:ow2-asm
+* ow2:ow2-asm-analysis
+* ow2:ow2-asm-commons
+* ow2:ow2-asm-tree
+* ow2:ow2-asm-util
+
+[[ow2_license]]
+----
+Copyright (c) 2000-2011 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[page_js]]
+page.js
+
+* js:page
+
+[[page_js_license]]
+----
+(The MIT License)
+
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[polymer]]
+polymer
+
+* js:font-roboto
+* js:iron-a11y-announcer
+* js:iron-a11y-keys-behavior
+* js:iron-autogrow-textarea
+* js:iron-behaviors
+* js:iron-checked-element-behavior
+* js:iron-dropdown
+* js:iron-fit-behavior
+* js:iron-flex-layout
+* js:iron-form-element-behavior
+* js:iron-icon
+* js:iron-iconset-svg
+* js:iron-input
+* js:iron-menu-behavior
+* js:iron-meta
+* js:iron-overlay-behavior
+* js:iron-resizable-behavior
+* js:iron-selector
+* js:iron-validatable-behavior
+* js:neon-animation
+* js:paper-behaviors
+* js:paper-button
+* js:paper-icon-button
+* js:paper-input
+* js:paper-item
+* js:paper-listbox
+* js:paper-ripple
+* js:paper-styles
+* js:paper-tabs
+* js:paper-toggle-button
+* js:polymer
+* js:polymer-resin
+* js:webcomponentsjs
+
+[[polymer_license]]
+----
+Copyright (c) 2014 The Polymer Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
+[[prologcafe]]
+prologcafe
+
+* prolog:cafeteria
+* prolog:compiler
+* prolog:io
+* prolog:runtime
+
+[[prologcafe_license]]
+----
+Prolog Cafe (A Prolog to Java Translator System)
+Copyright (C) 1997-2009 by Mutsunori Banbara and Naoyuki Tamura
+
+Prolog Cafe is free software; you can redistribute it and/or modify
+it under the terms of either:
+
+  * the GNU General Public License as published by the Free Software
+    Foundation; either version 2 of the License, or (at your option)
+    any later version, or
+
+  * the Eclipse Public License
+----
+
+In the context of Gerrit Code Review, Prolog Cafe is consumed under
+the <<prologcafe_EPL,EPL>>. Gerrit Code Review uses a fork derived
+from the 1.2.5 release and offers the corresponding source code at
+link:https://gerrit.googlesource.com/prolog-cafe[].
+
+----
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+----
+
+[[prologcafe_EPL]]
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+
+----
+
+
+[[promise-polyfill]]
+promise-polyfill
+
+* js:promise-polyfill
+
+[[promise-polyfill_license]]
+----
+Copyright (c) 2014 Taylor Hakes
+Copyright (c) 2014 Forbes Lindesay
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+----
+
+
+[[protobuf]]
+protobuf
+
+* protobuf
+
+[[protobuf_license]]
+----
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+    * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it.  This code is not
+standalone and requires a support library to be linked with it.  This
+support library is itself covered by the above license.
+
+----
+
+
+[[slf4j]]
+slf4j
+
+* log:api
+* log:jcl-over-slf4j
+
+[[slf4j_license]]
+----
+Copyright (c) 2004-2008 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free  of charge, to any person obtaining
+a  copy  of this  software  and  associated  documentation files  (the
+"Software"), to  deal in  the Software without  restriction, including
+without limitation  the rights to  use, copy, modify,  merge, publish,
+distribute,  sublicense, and/or sell  copies of  the Software,  and to
+permit persons to whom the Software  is furnished to do so, subject to
+the following conditions:
+
+The  above  copyright  notice  and  this permission  notice  shall  be
+included in all copies or substantial portions of the Software.
+
+THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+
+[[xz]]
+xz
+
+* tukaani-xz
+
+[[xz_license]]
+----
+All the files in this package have been written by Lasse Collin
+and/or Igor Pavlov. All these files have been put into the
+public domain. You can do whatever you want with these files.
+This software is provided "as is", without any warranty.
+
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index ced4609..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.
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..ff62da1 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -35,6 +35,11 @@
 
 The following endpoints are available to plugins.
 
+=== banner
+The `banner` extension point is located at the top of all pages. The purpose
+is to allow plugins to show outage information and important announcements to
+all users.
+
 === change-view-integration
 The `change-view-integration` extension point is located between `Files` and
 `Messages` section on the change view page, and it may take full page's
@@ -141,3 +146,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-init.txt b/Documentation/pgm-init.txt
index 9a16cdf..f6c3c85 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -28,7 +28,7 @@
 into a newly created `$site_path`.
 
 If run in an existing `$site_path`, init upgrades existing resources
-(e.g. DB schema, plugins) as necessary.
+(e.g. NoteDb schema, plugins) as necessary.
 
 == OPTIONS
 -b::
@@ -100,8 +100,7 @@
 	folder.
 
 == CONTEXT
-This command can only be run on a server which has direct
-connectivity to the metadata database, and local access to the
+This command can only be run on a server which has direct local access to the
 managed Git repositories.
 
 GERRIT
diff --git a/Documentation/pgm-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
index a669aa7..3566b8f 100644
--- a/Documentation/pgm-prolog-shell.txt
+++ b/Documentation/pgm-prolog-shell.txt
@@ -7,7 +7,7 @@
 [verse]
 --
 _java_ -jar gerrit.war _prolog-shell_
-  [-s FILE.pl ...]
+  [-q] [-s FILE.pl ...]
 --
 
 == DESCRIPTION
@@ -15,6 +15,8 @@
 and testing.
 
 == OPTIONS
+-q::
+	Do not display banner.
 -s::
 	Dynamically load the Prolog source code at startup,
 	as though the user had entered `['FILE.pl'].` into
diff --git a/Documentation/pgm-rulec.txt b/Documentation/pgm-rulec.txt
index 1b50812..2a987205 100644
--- a/Documentation/pgm-rulec.txt
+++ b/Documentation/pgm-rulec.txt
@@ -33,8 +33,7 @@
 	Compile rules for the specified project.
 
 == CONTEXT
-This command can only be run on a server which has direct
-connectivity to the metadata database, and local access to the
+This command can only be run on a server which has local access to the
 managed Git repositories.
 
 Caching needs to be enabled. See
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 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..9a23a27 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -725,6 +725,9 @@
 if the `cut` in the first rule is not reached and it only happens if a
 predicate before the `cut` fails.
 
+This fact can be bypassed by users who have
+link:access-control.html#category_forge_author[Forge Author] permission.
+
 ==== Don't use `gerrit:default_submit`
 Let's implement the same submit rule the other way, without reusing the
 `gerrit:default_submit`:
@@ -1044,10 +1047,7 @@
     gerrit:uploader(U),
     R = label('Is-Pure-Revert', ok(U)).
 
-submit_rule(submit(R)) :-
-    gerrit:pure_revert(U),
-    U /= 1,
-    R = label('Is-Pure-Revert', need(_)).
+submit_rule(submit(label('Is-Pure-Revert', need(_)))).
 ----
 
 Suppose currently a change is submittable if it gets `+2` for `Code-Review`
@@ -1058,21 +1058,20 @@
 [source,prolog]
 ----
 submit_rule(submit(CR, V, R)) :-
-    base(CR, V),
-    gerrit:pure_revert(1),
-    !,
-    gerrit:uploader(U),
-    R = label('Is-Pure-Revert', ok(U)).
-
-submit_rule(submit(CR, V, R)) :-
-    base(CR, V),
-    gerrit:pure_revert(U),
-    U /= 1,
-    R = label('Is-Pure-Revert', need(_)).
+  base(CR, V),
+  set_pure_revert_label(R).
 
 base(CR, V) :-
-    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
-    gerrit:max_with_block(-1, 1, 'Verified', V).
+  gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+  gerrit:max_with_block(-1, 1, 'Verified', V).
+
+set_pure_revert_label(R) :-
+  gerrit:pure_revert(1),
+  !,
+  gerrit:uploader(U),
+  R = label('Is-Pure-Revert', ok(U)).
+
+set_pure_revert_label(label('Is-Pure-Revert', need(_))).
 ----
 
 Note that a new label as `Is-Pure-Revert` should not be configured.
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 309a135..7d8ea23 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -87,6 +87,12 @@
   id="searchBox">
   Search
 </button>
+  %s
+</div>
+++++
+"""
+
+BUILTIN_SEARCH = """
 <script type="text/javascript">
 var f = function() {
   window.location = '../#/Documentation/q/' +
@@ -99,11 +105,25 @@
   }
 }
 </script>
-</div>
-++++
-
 """
 
+GOOGLE_SITE_SEARCH = """
+<script type="text/javascript">
+var f = function() {
+  window.location = 'https://www.google.com/search?q=' +
+     encodeURIComponent(document.getElementById("docSearch").value +
+     ' site:@SITE@');
+}
+document.getElementById("searchBox").onclick = f;
+document.getElementById("docSearch").onkeypress = function(e) {
+  if (13 == (e.keyCode ? e.keyCode : e.which)) {
+    f();
+  }
+}
+</script>
+"""
+
+
 LINK_SCRIPT = """
 
 ++++
@@ -227,8 +247,19 @@
                 help="generate the search boxes")
 opts.add_option('--no-searchbox', action="store_false", dest='searchbox',
                 help="don't generate the search boxes")
+opts.add_option('--site-search', action="store", metavar="SITE",
+                help=("generate the search box using google. SITE should " +
+                      "point to the domain/path of the site, eg. " +
+                      "gerrit-review.googlesource.com/Documentation"))
 options, _ = opts.parse_args()
 
+if options.site_search:
+  SEARCH_BOX = (SEARCH_BOX %
+                GOOGLE_SITE_SEARCH.replace("@SITE@", options.site_search))
+else:
+  SEARCH_BOX = SEARCH_BOX % BUILTIN_SEARCH
+
+
 try:
     try:
         out_file = open(options.out, 'w', errors='ignore')
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 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 ccec37b1..ad5ea82 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.
@@ -2454,7 +2467,7 @@
 Retrieves a change message including link:#detailed-accounts[detailed account information].
 
 --
-'GET /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}]'
+'GET /changes/link:#change-id[\{change-id\}]/messages/link:#change-message-id[\{change-message-id\}]'
 --
 
 As response a link:#change-message-info[ChangeMessageInfo] entity is returned.
@@ -2483,8 +2496,8 @@
 [[delete-change-message]]
 === Delete Change Message
 --
-'DELETE /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}]' +
-'POST /changes/link:#change-id[\{change-id\}]//message/link:#change-message-id[\{change-message-id\}]/delete'
+'DELETE /changes/link:#change-id[\{change-id\}]/messages/link:#change-message-id[\{change-message-id\}]' +
+'POST /changes/link:#change-id[\{change-id\}]/messages/link:#change-message-id[\{change-message-id\}]/delete'
 --
 
 Deletes a change message by replacing the change message with a new message,
@@ -2500,14 +2513,14 @@
 
 .Request
 ----
-  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780 HTTP/1.0
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/messages/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780 HTTP/1.0
 ----
 
 To provide a reason for the deletion, use a POST request:
 
 .Request
 ----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780/delete HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/messages/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780/delete HTTP/1.0
   Content-Type: application/json; charset=UTF-8
 
   {
@@ -2572,30 +2585,30 @@
 
   )]}'
   {
-    "commit":{
-      "parents":[
+    "commit": {
+      "parents": [
         {
-          "commit":"1eee2c9d8f352483781e772f35dc586a69ff5646",
+          "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
         }
       ],
-      "author":{
-        "name":"Shawn O. Pearce",
-        "email":"sop@google.com",
-        "date":"2012-04-24 18:08:08.000000000",
-        "tz":-420
+      "author": {
+        "name": "Shawn O. Pearce",
+        "email": "sop@google.com",
+        "date": "2012-04-24 18:08:08.000000000",
+        "tz": -420
        },
-       "committer":{
-         "name":"Shawn O. Pearce",
-         "email":"sop@google.com",
-         "date":"2012-04-24 18:08:08.000000000",
-         "tz":-420
+       "committer": {
+         "name": "Shawn O. Pearce",
+         "email": "sop@google.com",
+         "date": "2012-04-24 18:08:08.000000000",
+         "tz": -420
        },
-       "subject":"Use an EventBus to manage star icons",
-       "message":"Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+       "subject": "Use an EventBus to manage star icons",
+       "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
     },
-    "base_patch_set_number":1,
-    "base_revision":"c35558e0925e6985c91f3a16921537d5e572b7a3",
-    "ref":"refs/users/01/1000001/edit-76482/1"
+    "base_patch_set_number": 1,
+    "base_revision": "c35558e0925e6985c91f3a16921537d5e572b7a3",
+    "ref": "refs/users/01/1000001/edit-76482/1"
   }
 ----
 
@@ -2783,7 +2796,7 @@
 
   )]}'
   {
-  "web_links":[
+  "web_links": [
     {
       "show_on_side_by_side_diff_view": true,
       "name": "side-by-side preview diff",
@@ -4806,30 +4819,30 @@
 
     )]}'
     {
-      "commit":{
-        "parents":[
+      "commit": {
+        "parents": [
           {
-            "commit":"1eee2c9d8f352483781e772f35dc586a69ff5646",
+            "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
           }
         ],
-        "author":{
-          "name":"John Doe",
-          "email":"john.doe@example.com",
-          "date":"2013-05-07 15:21:27.000000000",
-          "tz":120
+        "author": {
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "date": "2013-05-07 15:21:27.000000000",
+          "tz": 120
          },
-         "committer":{
-           "name":"Jane Doe",
-           "email":"jane.doe@example.com",
-           "date":"2013-05-07 15:35:43.000000000",
-           "tz":120
+         "committer": {
+           "name": "Jane Doe",
+           "email": "jane.doe@example.com",
+           "date": "2013-05-07 15:35:43.000000000",
+           "tz": 120
          },
-         "subject":"Implement feature X",
-         "message":"Implement feature X\n\nWith this feature ..."
+         "subject": "Implement feature X",
+         "message": "Implement feature X\n\nWith this feature ..."
       },
-      "base_patch_set_number":1,
-      "base_revision":"674ac754f91e64a0efb8087e59a176484bd534d1"
-      "ref":"refs/users/01/1000001/edit-42622/1"
+      "base_patch_set_number": 1,
+      "base_revision": "674ac754f91e64a0efb8087e59a176484bd534d1"
+      "ref": "refs/users/01/1000001/edit-42622/1"
     }
 ----
 
@@ -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].
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 238eda6..f69c4ae 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -288,11 +288,11 @@
     },
     "child-project": {
       "id": "child-project",
-      "parent":"parent-project"
+      "parent": "parent-project"
     },
     "parent-project": {
       "id": "parent-project",
-      "parent":"All-Projects"
+      "parent": "All-Projects"
     }
   }
 ----
@@ -1271,14 +1271,14 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "add":{
-      "refs/heads/*":{
-        "permissions":{
-          "read":{
-            "rules":{
+    "add": {
+      "refs/heads/*": {
+        "permissions": {
+          "read": {
+            "rules": {
               "global:Anonymous-Users": {
-                "action":"DENY",
-                "force":false
+                "action": "DENY",
+                "force": false
               }
             }
           }
@@ -3135,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]
@@ -3626,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`,
@@ -3689,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/user-search-projects.txt b/Documentation/user-search-projects.txt
index 11c1326..8ebbf3e 100644
--- a/Documentation/user-search-projects.txt
+++ b/Documentation/user-search-projects.txt
@@ -12,6 +12,11 @@
 +
 Matches projects that have exactly the name 'NAME'.
 
+[[parent]]
+parent:'PARENT'::
++
+Matches projects that have 'PARENT' as parent project.
+
 [[inname]]
 inname:'NAME'::
 +
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 7c904f5..cafd5ca 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -216,7 +216,8 @@
 [[hashtag]]
 hashtag:'HASHTAG'::
 +
-Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG' exactly.
+Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG'.
+The match is case-insensitive.
 
 [[ref]]
 ref:'REF'::
@@ -276,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'::
@@ -289,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 6f89ef6..2a0c1ec 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -14,9 +14,23 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    sha256 = "4f2c173ebf95e94d98a0d5cb799e734536eaf3eca280eb15e124f5e5ef8b6e39",
-    strip_prefix = "rules_closure-6fd76e645b5c622221c9920f41a4d0bc578a3046",
-    urls = ["https://github.com/bazelbuild/rules_closure/archive/6fd76e645b5c622221c9920f41a4d0bc578a3046.tar.gz"],
+    sha256 = "34abd9170fdbfdfc6f3b63f2c18cee3cbcb2ddbd5e3c97324add0aa7809ed875",
+    strip_prefix = "rules_closure-9d543facf886631e4ed379996e60ce3533188adc",
+    urls = ["https://github.com/bazelbuild/rules_closure/archive/9d543facf886631e4ed379996e60ce3533188adc.tar.gz"],
+)
+
+# Transitive dependency of rules_closure and protobuf
+http_archive(
+    name = "net_zlib",
+    build_file = "//:lib/zlib/BUILD",
+    sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1",
+    strip_prefix = "zlib-1.2.11",
+    urls = ["https://zlib.net/zlib-1.2.11.tar.gz"],
+)
+
+bind(
+    name = "zlib",
+    actual = "@net_zlib//:zlib",
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -31,7 +45,7 @@
 
 load("@bazel_skylib//lib:versions.bzl", "versions")
 
-versions.check(minimum_bazel_version = "0.19.0")
+versions.check(minimum_bazel_version = "0.22.0")
 
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
 
@@ -47,11 +61,11 @@
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "ee5fe78fe417c685ecb77a0a725dc9f6040ae5beb44a0ba4ddb55453aad23a8a",
-    url = "https://github.com/bazelbuild/rules_go/releases/download/0.16.0/rules_go-0.16.0.tar.gz",
+    sha256 = "6776d68ebb897625dead17ae510eac3d5f6342367327875210df44dbe2aeeb19",
+    url = "https://github.com/bazelbuild/rules_go/releases/download/0.17.1/rules_go-0.17.1.tar.gz",
 )
 
-load("@io_bazel_rules_go//go:def.bzl", "go_register_toolchains", "go_rules_dependencies")
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
 
 go_rules_dependencies()
 
@@ -59,8 +73,8 @@
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "c0a5739d12c6d05b6c1ad56f2200cb0b57c5a70e03ebd2f7b87ce88cabf09c7b",
-    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.14.0/bazel-gazelle-0.14.0.tar.gz"],
+    sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
+    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
 )
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
@@ -69,12 +83,6 @@
 
 # Dependencies for PolyGerrit local dev server.
 go_repository(
-    name = "com_github_robfig_soy",
-    commit = "82face14ebc0883b4ca9c901b5aaf3738b9f6a24",
-    importpath = "github.com/robfig/soy",
-)
-
-go_repository(
     name = "com_github_howeyc_fsnotify",
     commit = "441bbc86b167f3c1f4786afae9931403b99fdacf",
     importpath = "github.com/howeyc/fsnotify",
@@ -156,18 +164,18 @@
     sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
 )
 
-FLOGGER_VERS = "0.3.1"
+FLOGGER_VERS = "0.4"
 
 maven_jar(
     name = "flogger",
     artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
-    sha1 = "585030fe1ec709760cbef997a459729fb965df0e",
+    sha1 = "9c8863dcc913b56291c0c88e6d4ca9715b43df98",
 )
 
 maven_jar(
     name = "flogger-log4j-backend",
     artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
-    sha1 = "d5085e3996bddc4b105d53b886190cc9a8811a9e",
+    sha1 = "17aa5e31daa1354187e14b6978597d630391c028",
 )
 
 maven_jar(
@@ -183,16 +191,9 @@
 )
 
 maven_jar(
-    name = "gwtorm-client",
-    artifact = "com.google.gerrit:gwtorm:1.20",
-    sha1 = "a4809769b710bc8ce3f203125630b8419f0e58b0",
-    src_sha1 = "cb63296276ce3228b2d83a37017a99e38ad8ed42",
-)
-
-maven_jar(
     name = "protobuf",
-    artifact = "com.google.protobuf:protobuf-java:3.6.1",
-    sha1 = "0d06d46ecfd92ec6d0f3b423b4cd81cb38d8b924",
+    artifact = "com.google.protobuf:protobuf-java:3.7.1",
+    sha1 = "0bce1b6dc9e4531169542ab37a1c8641bcaa8afb",
 )
 
 load("//lib:guava.bzl", "GUAVA_BIN_SHA1", "GUAVA_VERSION")
@@ -227,30 +228,30 @@
     sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
 )
 
-SLF4J_VERS = "1.7.7"
+SLF4J_VERS = "1.7.26"
 
 maven_jar(
     name = "log-api",
     artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
-    sha1 = "2b8019b6249bb05d81d3a3094e468753e2b21311",
+    sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
 )
 
 maven_jar(
     name = "log-ext",
     artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
-    sha1 = "09a8f58c784c37525d2624062414358acf296717",
+    sha1 = "31cdf122e000322e9efcb38913e9ab07825b17ef",
 )
 
 maven_jar(
     name = "impl-log4j",
     artifact = "org.slf4j:slf4j-log4j12:" + SLF4J_VERS,
-    sha1 = "58f588119ffd1702c77ccab6acb54bfb41bed8bd",
+    sha1 = "12f5c685b71c3027fd28bcf90528ec4ec74bf818",
 )
 
 maven_jar(
     name = "jcl-over-slf4j",
     artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
-    sha1 = "56003dcd0a31deea6391b9e2ef2f2dc90b205a92",
+    sha1 = "33fbc2d93de829fa5e263c5ce97f5eab8f57d53e",
 )
 
 maven_jar(
@@ -303,6 +304,12 @@
 )
 
 maven_jar(
+    name = "commons-text",
+    artifact = "org.apache.commons:commons-text:1.2",
+    sha1 = "74acdec7237f576c4803fff0c1008ab8a3808b2b",
+)
+
+maven_jar(
     name = "commons-dbcp",
     artifact = "commons-dbcp:commons-dbcp:1.4",
     sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39",
@@ -334,6 +341,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(
@@ -486,7 +520,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",
@@ -561,18 +595,18 @@
     sha1 = "18d4d07010c24405129a6dbb0e92057f8779fb9d",
 )
 
-AUTO_VALUE_VERSION = "1.6.3"
+AUTO_VALUE_VERSION = "1.6.5"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "8edb6675b9c09ffdcc19937428e7ef1e3d066e12",
+    sha1 = "816872c85048f36a67a276ef7a49cc2e4595711c",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "b88c1bb7f149f6d2cc03898359283e57b08f39cc",
+    sha1 = "c3dad10377f0e2242c9a4b88e9704eaf79103679",
 )
 
 # Transitive dependency of commons-compress
@@ -621,7 +655,7 @@
     sha1 = "0c9cfae15c74f62491d4f28def0dff1dabe52a47",
 )
 
-PROLOG_VERS = "1.4.3"
+PROLOG_VERS = "1.4.4"
 
 PROLOG_REPO = GERRIT
 
@@ -630,7 +664,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-runtime:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "d5206556cbc76ffeab21313ffc47b586a1efbcbb",
+    sha1 = "e9a364f4233481cce63239e8e68a6190c8f58acd",
 )
 
 maven_jar(
@@ -638,7 +672,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "f37032cf1dec3e064427745bc59da5a12757a3b2",
+    sha1 = "570295026f6aa7b905e423d107cb2e081eecdc04",
 )
 
 maven_jar(
@@ -646,7 +680,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "d02b2640b26f64036b6ba2b45e4acc79281cea17",
+    sha1 = "1f25c4e27d22bdbc31481ee0c962a2a2853e4428",
 )
 
 maven_jar(
@@ -654,7 +688,7 @@
     artifact = "com.googlecode.prolog-cafe:prolog-cafeteria:" + PROLOG_VERS,
     attach_source = False,
     repository = PROLOG_REPO,
-    sha1 = "e3b1860c63e57265e5435f890263ad82dafa724f",
+    sha1 = "0e6c2deeaf5054815a561cbd663566fd59b56c6c",
 )
 
 maven_jar(
@@ -669,14 +703,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",
@@ -698,8 +748,8 @@
 
 maven_jar(
     name = "dropwizard-core",
-    artifact = "io.dropwizard.metrics:metrics-core:4.0.3",
-    sha1 = "bb562ee73f740bb6b2bf7955f97be6b870d9e9f0",
+    artifact = "io.dropwizard.metrics:metrics-core:4.0.5",
+    sha1 = "b81ef162970cdb9f4512ee2da09715a856ff4c4c",
 )
 
 # When updating Bouncy Castle, also update it in bazlets.
@@ -779,15 +829,15 @@
 # elasticsearch-rest-client explicitly depends on this version
 maven_jar(
     name = "httpasyncclient",
-    artifact = "org.apache.httpcomponents:httpasyncclient:4.1.2",
-    sha1 = "95aa3e6fb520191a0970a73cf09f62948ee614be",
+    artifact = "org.apache.httpcomponents:httpasyncclient:4.1.4",
+    sha1 = "f3a3240681faae3fa46b573a4c7e50cec9db0d86",
 )
 
 # elasticsearch-rest-client explicitly depends on this version
 maven_jar(
     name = "httpcore-nio",
-    artifact = "org.apache.httpcomponents:httpcore-nio:4.4.5",
-    sha1 = "f4be009e7505f6ceddf21e7960c759f413f15056",
+    artifact = "org.apache.httpcomponents:httpcore-nio:4.4.11",
+    sha1 = "7d0a97d01d39cff9aa3e6db81f21fddb2435f4e6",
 )
 
 # Test-only dependencies below.
@@ -810,30 +860,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.42"
+TRUTH_VERS = "0.44"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "b5768f644b114e6cf5c3962c2ebcb072f788dcbb",
+    sha1 = "11eff954c0c14da7d43276d7b3bcf71463105368",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "4d01dfa5b3780632a3d109e14e101f01d10cce2c",
+    sha1 = "2081a0721d3101e1cf559f013e59c6129b4b10b0",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "c231e6735aa6c133c7e411ae1c1c90b124900a8b",
+    sha1 = "64f47e4e3f79b0a582573098b9c3c6b73599f7c6",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "c41d22e8b4a61b4171e57c44a2959ebee0091a14",
+    sha1 = "c03fbc16087d8cb3bf0f3265a04566d4beb88a6d",
 )
 
 maven_jar(
@@ -912,60 +962,60 @@
     sha1 = "75070c744a8e52a7d17b8b476468580309d5cd09",
 )
 
-JETTY_VERS = "9.4.12.v20180830"
+JETTY_VERS = "9.4.14.v20181114"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "4c1149328eda9fa39a274262042420f66d9ffd5f",
+    sha1 = "96f501462af425190ff7b63e387692c1aa3af2c8",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "299e0602a9c0b753ba232cc1c1dda72ddd9addcf",
+    sha1 = "6cbeb2fe9b3cc4f88a7ea040b8a0c4f703cd72ce",
 )
 
 maven_jar(
     name = "jetty-servlets",
     artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS,
-    sha1 = "53745200718fe4ddf57f04ad3ba34778a6aca585",
+    sha1 = "38cfc07b53e5d285bb2fca78bb2531565ed9c9e5",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "b0f25df0d32a445fd07d5f16fff1411c16b888fa",
+    sha1 = "b36a3d52d78a1df6406f6fa236a6eeff48cbfef6",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "7e9e589dd749a8c096008c0c4af863a81e67c55b",
+    sha1 = "3e02463d2bff175a3231cd3dc26363eaf76a3b17",
 )
 
 maven_jar(
     name = "jetty-continuation",
     artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS,
-    sha1 = "5f6d6e06f95088a3a7118b9065bc49ce7c014b75",
+    sha1 = "ac4981a61bcaf4e2538de6270300a870224a16b8",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "1341796dde4e16df69bca83f3e87688ba2e7d703",
+    sha1 = "6d0c8ac42e9894ae7b5032438eb4579c2a47f4fe",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "e93f5adaa35a9a6a85ba130f589c5305c6ecc9e3",
+    sha1 = "a8c6a705ddb9f83a75777d89b0be59fcef3f7637",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "cb4ccec9bd1fe4b10a04a0fb25d7053c1050188a",
+    sha1 = "5bb3d7a38f7ea54138336591d89dd5867b806c02",
 )
 
 maven_jar(
@@ -1005,12 +1055,12 @@
     sha1 = "76716d529710fc03d1d429b43e3cedd4419f78d4",
 )
 
-# When upgrading elasticsearch-rest-client, also upgrade http-niocore
+# When upgrading elasticsearch-rest-client, also upgrade httpcore-nio
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.5.4",
-    sha1 = "552175b06e34df96f114d1c8aaa908e535c8f1be",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.0.0",
+    sha1 = "121d12f1c71f318be1a654e8a956e38d5b68e98a",
 )
 
 JACKSON_VERSION = "2.9.8"
@@ -1021,18 +1071,18 @@
     sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
 )
 
-TESTCONTAINERS_VERSION = "1.10.3"
+TESTCONTAINERS_VERSION = "1.11.2"
 
 maven_jar(
     name = "testcontainers",
     artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-    sha1 = "e561ce99fc616b383d85f35ce881e58e8de59ae7",
+    sha1 = "eae47ed24bb07270d4b60b5e2c3444c5bf3c8ea9",
 )
 
 maven_jar(
     name = "testcontainers-elasticsearch",
     artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-    sha1 = "0cb114ecba0ed54a116e2be2f031bc45ca4cbfc8",
+    sha1 = "a327bd8cb68eb7146b36d754aee98a8018132d8f",
 )
 
 maven_jar(
@@ -1043,14 +1093,14 @@
 
 maven_jar(
     name = "visible-assertions",
-    artifact = "org.rnorth.visible-assertions:visible-assertions:2.1.0",
-    sha1 = "f2fcff2862860828ac38a5e1f14d941787c06b13",
+    artifact = "org.rnorth.visible-assertions:visible-assertions:2.1.2",
+    sha1 = "20d31a578030ec8e941888537267d3123c2ad1c1",
 )
 
 maven_jar(
     name = "jna",
-    artifact = "net.java.dev.jna:jna:4.5.1",
-    sha1 = "65bd0cacc9c79a21c6ed8e9f588577cd3c2f85b9",
+    artifact = "net.java.dev.jna:jna:5.2.0",
+    sha1 = "ed8b772eb077a9cb50e44e90899c66a9a6c00e67",
 )
 
 maven_jar(
@@ -1061,22 +1111,22 @@
 
 maven_jar(
     name = "mockito",
-    artifact = "org.mockito:mockito-core:2.23.4",
-    sha1 = "a35b6f8ffcfa786771eac7d7d903429e790fdf3f",
+    artifact = "org.mockito:mockito-core:2.24.0",
+    sha1 = "969a7bcb6f16e076904336ebc7ca171d412cc1f9",
 )
 
-BYTE_BUDDY_VERSION = "1.9.3"
+BYTE_BUDDY_VERSION = "1.9.7"
 
 maven_jar(
     name = "byte-buddy",
     artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
-    sha1 = "f32e510b239620852fc9a2387fac41fd053d6a4d",
+    sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
 )
 
 maven_jar(
     name = "byte-buddy-agent",
     artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
-    sha1 = "f5b78c16cf4060664d80b6ca32d80dca4bd3d264",
+    sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
 )
 
 maven_jar(
@@ -1247,10 +1297,17 @@
 )
 
 bower_archive(
+    name = "resemblejs",
+    package = "rsmbl/Resemble.js",
+    sha1 = "49d5f022417c389b630d6f7ee667aa9540075c42",
+    version = "2.10.1",
+)
+
+bower_archive(
     name = "codemirror-minified",
     package = "Dominator008/codemirror-minified",
-    sha1 = "1524e19087d8223edfe4a5b1ccf04c1e3707235d",
-    version = "5.37.0",
+    sha1 = "e6bda82afc7cf3493f4282c6f17265d40e1485e5",
+    version = "5.43.0",
 )
 
 # bower test stuff
diff --git a/antlr3/BUILD b/antlr3/BUILD
index fc96715..2d3050e 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -20,7 +20,8 @@
     name = "query_parser",
     srcs = [":query"],
     visibility = [
-        "//java/com/google/gerrit/index:__pkg__",
+        "//java/com/google/gerrit/index:__subpackages__",
+        "//javatests/com/google/gerrit:__subpackages__",
         "//javatests/com/google/gerrit/index:__pkg__",
         "//plugins:__pkg__",
     ],
diff --git a/antlr3/com/google/gerrit/index/query/Query.g b/antlr3/com/google/gerrit/index/query/Query.g
index 953a473..1bf20aa 100644
--- a/antlr3/com/google/gerrit/index/query/Query.g
+++ b/antlr3/com/google/gerrit/index/query/Query.g
@@ -120,12 +120,24 @@
   ;
 conditionBase
   : '('! conditionOr ')'!
-  | (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue
+  | (FIELD_NAME COLON) => FIELD_NAME^ COLON! fieldValue
   | fieldValue -> ^(DEFAULT_FIELD fieldValue)
   ;
 
 fieldValue
-  : n=FIELD_NAME   -> SINGLE_WORD[n]
+  // Rewrite by invoking SINGLE_WORD fragment lexer rule, passing the field name as an argument.
+  : n=FIELD_NAME -> SINGLE_WORD[n]
+
+  // Allow field values to contain a colon. We can't do this at the lexer level, because we need to
+  // emit a separate token for the field name. If we were to allow ':' in SINGLE_WORD, then
+  // everything would just lex as DEFAULT_FIELD.
+  //
+  // Field values with a colon may be lexed either as <field>:<rest> or <word>:<rest>, depending on
+  // whether the part before the colon looks like a field name.
+  // TODO(dborowitz): Field values ending in colon still don't work.
+  | (FIELD_NAME COLON) => n=FIELD_NAME COLON fieldValue -> SINGLE_WORD[n] COLON fieldValue
+  | (SINGLE_WORD COLON) => SINGLE_WORD COLON fieldValue
+
   | SINGLE_WORD
   | EXACT_PHRASE
   ;
@@ -134,6 +146,8 @@
 OR:  'OR'  ;
 NOT: 'NOT' ;
 
+COLON: ':' ;
+
 WS
   :  ( ' ' | '\r' | '\t' | '\n' ) { $channel=HIDDEN; }
   ;
@@ -172,7 +186,7 @@
      // '-'  permit
      // '.'  permit
      // '/'  permit
-     | ':'
+     | COLON
      | ';'
      // '<' permit
      // '=' permit
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
deleted file mode 100755
index 2e01131..0000000
--- a/contrib/abandon_stale.py
+++ /dev/null
@@ -1,225 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# The MIT License
-#
-# Copyright 2014 Sony Mobile Communications. All rights reserved.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-""" Script to abandon stale changes from the review server.
-
-Fetches a list of open changes that have not been updated since a given age in
-days, months or years (default 6 months), and then abandons them.
-
-Requires the user's credentials for the Gerrit server to be declared in the
-.netrc file. Supports either basic or digest authentication.
-
-Example to abandon changes that have not been updated for 3 months:
-
-  ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
-
-Supports dry-run mode to only list the stale changes, but not actually
-abandon them.
-
-See the --help output for more information about options.
-
-Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2) to be installed
-and available for import.
-
-"""
-
-import logging
-import optparse
-import re
-import sys
-
-from pygerrit2.rest import GerritRestAPI
-from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
-
-
-def _main():
-    parser = optparse.OptionParser()
-    parser.add_option('-g', '--gerrit-url', dest='gerrit_url',
-                      metavar='URL',
-                      default=None,
-                      help='gerrit server URL')
-    parser.add_option('-b', '--basic-auth', dest='basic_auth',
-                      action='store_true',
-                      help='(deprecated) use HTTP basic authentication instead'
-                      ' of digest')
-    parser.add_option('-d', '--digest-auth', dest='digest_auth',
-                      action='store_true',
-                      help='use HTTP digest authentication instead of basic')
-    parser.add_option('-n', '--dry-run', dest='dry_run',
-                      action='store_true',
-                      help='enable dry-run mode: show stale changes but do '
-                           'not abandon them')
-    parser.add_option('-t', '--test', dest='testmode', action='store_true',
-                      help='test mode: query changes with the `test-abandon` '
-                           'topic and ignore age option')
-    parser.add_option('-a', '--age', dest='age',
-                      metavar='AGE',
-                      default="6months",
-                      help='age of change since last update in days, months'
-                           ' or years (default: %default)')
-    parser.add_option('-m', '--message', dest='message',
-                      metavar='STRING', default=None,
-                      help='custom message to append to abandon message')
-    parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
-                      default=[], action='append',
-                      help='abandon changes only on the given branch')
-    parser.add_option('--exclude-branch', dest='exclude_branches',
-                      metavar='BRANCH_NAME',
-                      default=[],
-                      action='append',
-                      help='do not abandon changes on given branch')
-    parser.add_option('--project', dest='projects', metavar='PROJECT_NAME',
-                      default=[], action='append',
-                      help='abandon changes only on the given project')
-    parser.add_option('--exclude-project', dest='exclude_projects',
-                      metavar='PROJECT_NAME',
-                      default=[],
-                      action='append',
-                      help='do not abandon changes on given project')
-    parser.add_option('--owner', dest='owner',
-                      metavar='USERNAME',
-                      default=None,
-                      action='store',
-                      help='only abandon changes owned by the given user')
-    parser.add_option('--exclude-wip', dest='exclude_wip',
-                      action='store_true',
-                      help='Exclude changes that are Work-in-Progress')
-    parser.add_option('-v', '--verbose', dest='verbose',
-                      action='store_true',
-                      help='enable verbose (debug) logging')
-
-    (options, _args) = parser.parse_args()
-
-    level = logging.DEBUG if options.verbose else logging.INFO
-    logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
-                        level=level)
-
-    if not options.gerrit_url:
-        logging.error("Gerrit URL is required")
-        return 1
-
-    if options.testmode:
-        message = "Abandoning in test mode"
-    else:
-        pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
-        match = pattern.match(options.age)
-        if not match:
-            logging.error("Invalid age: %s", options.age)
-            return 1
-        message = "Abandoning after %s %s or more of inactivity." % \
-            (match.group(1), match.group(2))
-
-    if options.digest_auth:
-        auth_type = HTTPDigestAuthFromNetrc
-    else:
-        auth_type = HTTPBasicAuthFromNetrc
-
-    try:
-        auth = auth_type(url=options.gerrit_url)
-        gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
-    except Exception as e:
-        logging.error(e)
-        return 1
-
-    logging.info(message)
-    try:
-        stale_changes = []
-        offset = 0
-        step = 500
-        if options.testmode:
-            query_terms = ["status:new", "owner:self", "topic:test-abandon"]
-        else:
-            query_terms = ["status:new", "age:%s" % options.age]
-        if options.exclude_wip:
-            query_terms += ["-is:wip"]
-        if options.branches:
-            query_terms += ["branch:%s" % b for b in options.branches]
-        elif options.exclude_branches:
-            query_terms += ["-branch:%s" % b for b in options.exclude_branches]
-        if options.projects:
-            query_terms += ["project:%s" % p for p in options.projects]
-        elif options.exclude_projects:
-            query_terms = ["-project:%s" % p for p in options.exclude_projects]
-        if options.owner and not options.testmode:
-            query_terms += ["owner:%s" % options.owner]
-        query = "%20".join(query_terms)
-        while True:
-            q = query + "&o=DETAILED_ACCOUNTS&n=%d&S=%d" % (step, offset)
-            logging.debug("Query: %s", q)
-            url = "/changes/?q=" + q
-            result = gerrit.get(url)
-            logging.debug("%d changes", len(result))
-            if not result:
-                break
-            stale_changes += result
-            last = result[-1]
-            if "_more_changes" in last:
-                logging.debug("More...")
-                offset += step
-            else:
-                break
-    except Exception as e:
-        logging.error(e)
-        return 1
-
-    abandoned = 0
-    errors = 0
-    abandon_message = message
-    if options.message:
-        abandon_message += "\n\n" + options.message
-    for change in stale_changes:
-        number = change["_number"]
-        project = ""
-        if len(options.projects) != 1:
-            project = "%s: " % change["project"]
-        owner = ""
-        if options.verbose:
-            try:
-                o = change["owner"]["name"]
-            except KeyError:
-                o = "Unknown"
-            owner = " (%s)" % o
-        subject = change["subject"]
-        if len(subject) > 70:
-            subject = subject[:65] + " [...]"
-        change_id = change["id"]
-        logging.info("%s%s: %s%s", number, owner, project, subject)
-        if options.dry_run:
-            continue
-
-        try:
-            gerrit.post("/changes/" + change_id + "/abandon",
-                        json={"message": "%s" % abandon_message})
-            abandoned += 1
-        except Exception as e:
-            errors += 1
-            logging.error(e)
-    logging.info("Total %d stale open changes", len(stale_changes))
-    if not options.dry_run:
-        logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
-
-
-if __name__ == "__main__":
-    sys.exit(_main())
diff --git a/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md
index ad23140..1ec8dd4 100644
--- a/contrib/mitm-ui/README.md
+++ b/contrib/mitm-ui/README.md
@@ -8,7 +8,10 @@
    cd ~/gerrit
    ~/mitm-gerrit/mitm-serve-app-dev.sh
    ```
-3. Install MITM certificates
+3. Make sure that the browser uses the proxy provided by the command line,
+   e.g. if you are a Googler check that the BeyondCorp extension uses the
+   "System/Alternative" proxy.
+4. Install MITM certificates
    - Open http://mitm.it in the proxied browser window
    - Follow the instructions to install MITM certs
 
diff --git a/contrib/mitm-ui/mitm-docker.sh b/contrib/mitm-ui/mitm-docker.sh
index 77f209e..a1206f7 100755
--- a/contrib/mitm-ui/mitm-docker.sh
+++ b/contrib/mitm-ui/mitm-docker.sh
@@ -36,6 +36,7 @@
        -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
        -v ${mitm_dir}:${mitm_dir} \
        -v ${gerrit_dir}:${gerrit_dir} \
+       -v ${gerrit_dir}/bazel-out:${gerrit_dir}/bazel-out \
        -v ${extra_volume} \
        -p 8888:8888 \
        mitmproxy/mitmproxy:2.0.2 \
diff --git a/contrib/mitm-ui/mitm-plugins.sh b/contrib/mitm-ui/mitm-plugins.sh
index 992ef07..fc542bb 100755
--- a/contrib/mitm-ui/mitm-plugins.sh
+++ b/contrib/mitm-ui/mitm-plugins.sh
@@ -30,4 +30,10 @@
 
 ${mitm_dir}/dev-chrome.sh &
 
-${mitm_dir}/mitm-docker.sh "serve-app-dev.py --plugins ${absolute_plugin_paths} --strip_assets"
+bazel build //polygerrit-ui/app:test_components &
+
+${mitm_dir}/mitm-docker.sh \
+           "serve-app-dev.py \
+           --plugins ${absolute_plugin_paths} \
+           --strip_assets \
+           --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-serve-app-dev.sh b/contrib/mitm-ui/mitm-serve-app-dev.sh
index 4fa8958..d4c72cc 100755
--- a/contrib/mitm-ui/mitm-serve-app-dev.sh
+++ b/contrib/mitm-ui/mitm-serve-app-dev.sh
@@ -8,6 +8,8 @@
 
 mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
 
+bazel build //polygerrit-ui/app:test_components &
+
 ${mitm_dir}/dev-chrome.sh &
 
-${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/"
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/ --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-single-plugin.sh b/contrib/mitm-ui/mitm-single-plugin.sh
index 4acae7f..8958229 100755
--- a/contrib/mitm-ui/mitm-single-plugin.sh
+++ b/contrib/mitm-ui/mitm-single-plugin.sh
@@ -28,4 +28,11 @@
 
 ${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}"
+bazel build //polygerrit-ui/app:test_components &
+
+${mitm_dir}/mitm-docker.sh -v ${plugin_root}:${plugin_root} \
+           "serve-app-dev.py \
+           --plugins ${plugin} \
+           --strip_assets \
+           --plugin_root ${plugin_root}  \
+           --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/serve-app-dev.py b/contrib/mitm-ui/serve-app-dev.py
index 18e9de1..cdf7bfc 100644
--- a/contrib/mitm-ui/serve-app-dev.py
+++ b/contrib/mitm-ui/serve-app-dev.py
@@ -28,16 +28,19 @@
 
 from mitmproxy import http
 from mitmproxy.script import concurrent
-import re
 import argparse
-import os.path
 import json
 import mimetypes
+import os.path
+import re
+import zipfile
 
 class Server:
-    def __init__(self, devpath, plugins, pluginroot, assets, strip_assets, theme):
+    def __init__(self, devpath, components, plugins, pluginroot, assets, strip_assets, theme):
         if devpath:
             print("Serving app from " + devpath)
+        if components:
+            print("Serving components from " + components)
         if pluginroot:
             print("Serving plugins from " + pluginroot)
         if assets:
@@ -52,6 +55,7 @@
         else:
             self.plugins = {}
         self.devpath = devpath
+        self.components = components
         self.pluginroot = pluginroot
         self.strip_assets = strip_assets
         self.theme = theme
@@ -92,6 +96,7 @@
     m = re.match(".+polygerrit_ui/\d+\.\d+/(.+)", flow.request.path)
     pluginmatch = re.match("^/plugins/(.+)", flow.request.path)
     localfile = ""
+    content = ""
     if flow.request.path == "/config/server/info":
         config = json.loads(flow.response.content[5:].decode('utf8'))
         if server.theme:
@@ -105,6 +110,9 @@
         flow.response.content = str.encode(")]}'\n" + json.dumps(config))
     if m is not None:
         filepath = m.groups()[0]
+        if (filepath.startswith("bower_components/")):
+            with zipfile.ZipFile(server.components + "test_components.zip") as bower_zip:
+                content = bower_zip.read(filepath)
         localfile = server.devpath + filepath
     elif pluginmatch is not None:
         pluginfile = flow.request.path_components[-1]
@@ -131,7 +139,10 @@
     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)
+        content = server.readfile(localfile)
+
+    if content:
+        flow.response.content = content
         flow.response.status_code = 200
         localtype = mimetypes.guess_type(localfile)
         if localtype and localtype[0]:
@@ -142,13 +153,15 @@
 
 parser = argparse.ArgumentParser()
 parser.add_argument("--app", type=str, default="", help="Path to /polygerrit-ui/app/")
+parser.add_argument("--components", type=str, default="", help="Path to test_components.zip")
 parser.add_argument("--plugins", type=str, default="", help="Comma-separated list of plugin files to add/replace")
 parser.add_argument("--plugin_root", type=str, default="", help="Path containing individual plugin files to replace")
 parser.add_argument("--assets", type=str, default="", help="Path containing assets file to import.")
 parser.add_argument("--strip_assets", action="store_true", help="Strip plugin bundles from the response.")
-parser.add_argument("--theme", type=str, help="Path to the default site theme to be used.")
+parser.add_argument("--theme", default="", type=str, help="Path to the default site theme to be used.")
 args = parser.parse_args()
 server = Server(expandpath(args.app) + '/',
+                expandpath(args.components) + '/',
                 args.plugins,
                 expandpath(args.plugin_root) + '/',
                 args.assets and expandpath(args.assets),
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8d05ea1..bb6a4b4 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
+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;
@@ -26,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;
 
@@ -49,6 +50,7 @@
 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;
@@ -86,6 +88,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 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;
@@ -101,21 +104,22 @@
 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;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexer;
 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.index.group.GroupIndexer;
 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.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;
@@ -128,7 +132,6 @@
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.SshMode;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -137,6 +140,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.Modifier;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
@@ -209,10 +213,7 @@
                 firstTest = description;
               }
               beforeTest(description);
-              ProjectResetter.Config input = resetProjects();
-              if (input == null) {
-                input = defaultResetProjects();
-              }
+              ProjectResetter.Config input = requireNonNull(resetProjects());
 
               try (ProjectResetter resetter = projectResetter.builder().build(input)) {
                 AbstractDaemonTest.this.resetter = resetter;
@@ -282,12 +283,13 @@
   @Inject private AccountIndexer accountIndexer;
   @Inject private ChangeIndexCollection changeIndexes;
   @Inject private EventRecorder.Factory eventRecorderFactory;
-  @Inject private GroupIndexer groupIndexer;
-  @Inject private Groups groups;
   @Inject private InProcessProtocol inProcessProtocol;
+  @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;
@@ -335,10 +337,6 @@
 
   /** Controls which project and branches should be reset after each test case. */
   protected ProjectResetter.Config resetProjects() {
-    return null;
-  }
-
-  private ProjectResetter.Config defaultResetProjects() {
     return new ProjectResetter.Config()
         // Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
         // not reused.
@@ -365,18 +363,11 @@
     initSsh();
   }
 
-  protected void evictAndReindexAccount(Account.Id accountId) throws IOException {
+  protected void evictAndReindexAccount(Account.Id accountId) {
     accountCache.evict(accountId);
     accountIndexer.index(accountId);
   }
 
-  private void reindexAllGroups() throws IOException, ConfigInvalidException {
-    Iterable<GroupReference> allGroups = groups.getAllGroupReferences()::iterator;
-    for (GroupReference group : allGroups) {
-      groupIndexer.index(group.getUUID());
-    }
-  }
-
   protected static Config submitWholeTopicEnabledConfig() {
     Config cfg = new Config();
     cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -420,24 +411,12 @@
     Transport.register(inProcessProtocol);
     toClose = Collections.synchronizedList(new ArrayList<>());
 
-    // All groups which were added during the server start (e.g. in SchemaCreatorImpl) aren't
-    // contained in the instance of the group index which is available here and in tests. There are
-    // two reasons:
-    // 1) No group index is available in SchemaCreatorImpl when using an in-memory database.
-    // (This could be fixed by using the IndexManagerOnInit in InMemoryTestingDatabaseModule similar
-    // to how BaseInit uses it.)
-    // 2) During the on-init part of the server start, we use another instance of the index than
-    // later on. As test indexes are non-permanent, closing an instance and opening another one
-    // removes all indexed data.
-    // As a workaround, we simply reindex all available groups here.
-    reindexAllGroups();
-
     admin = accountCreator.admin();
     user = accountCreator.user();
 
     // Evict and reindex accounts in case tests modify them.
-    evictAndReindexAccount(admin.getId());
-    evictAndReindexAccount(user.getId());
+    evictAndReindexAccount(admin.id());
+    evictAndReindexAccount(user.id());
 
     adminRestSession = new RestSession(server, admin);
     userRestSession = new RestSession(server, user);
@@ -455,7 +434,9 @@
     ProjectInput in = projectInput(description);
     gApi.projects().create(in);
     project = new Project.NameKey(in.name);
-    testRepo = cloneProject(project, getCloneAsAccount(description));
+    if (!classDesc.skipProjectClone()) {
+      testRepo = cloneProject(project, getCloneAsAccount(description));
+    }
   }
 
   /** Override to bind an additional Guice module */
@@ -571,7 +552,7 @@
   protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
       throws Exception {
     InProcessProtocol.Context ctx =
-        new InProcessProtocol.Context(identifiedUserFactory, testAccount.getId(), p);
+        new InProcessProtocol.Context(identifiedUserFactory, testAccount.id(), p);
     Repository repo = repoManager.openRepository(p);
     toClose.add(repo);
     return inProcessProtocol.register(ctx, repo).toString();
@@ -623,7 +604,7 @@
   }
 
   protected PushOneCommit.Result createChange(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     return result;
@@ -639,7 +620,7 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 1",
                 ImmutableMap.of(file, "foo-1", "bar", "bar-1"))
@@ -651,7 +632,7 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 2",
                 ImmutableMap.of(file, "foo-2", "bar", "bar-2"))
@@ -659,7 +640,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            admin.getIdent(), testRepo, "merge", ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
     m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
     PushOneCommit.Result result = m.to(ref);
     result.assertOkStatus();
@@ -674,7 +655,7 @@
       String content)
       throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), repo, commitMsg, fileName, content).to(ref);
+        pushFactory.create(admin.newIdent(), repo, commitMsg, fileName, content).to(ref);
     result.assertOkStatus();
     return result;
   }
@@ -693,7 +674,7 @@
 
   protected PushOneCommit.Result createChange(String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master");
   }
 
@@ -705,7 +686,7 @@
       String content,
       String topic)
       throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);
     return push.to("refs/for/" + branch + "%topic=" + name(topic));
   }
 
@@ -759,7 +740,7 @@
       String content)
       throws Exception {
     PushOneCommit push =
-        pushFactory.create(testAccount.getIdent(), repo, subject, fileName, content, changeId);
+        pushFactory.create(testAccount.newIdent(), repo, subject, fileName, content, changeId);
     return push.to(ref);
   }
 
@@ -785,7 +766,7 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    requestScopeOperations.setApiUser(account.getId());
+    requestScopeOperations.setApiUser(account.id());
     return atrScope.get();
   }
 
@@ -799,14 +780,13 @@
     return accountState.get();
   }
 
-  protected Context disableDb() {
+  protected AutoCloseable disableNoteDb() {
     changeNotesArgs.failOnLoadForTest.set(true);
-    return atrScope.disableDb();
-  }
-
-  protected void enableDb(Context preDisableContext) {
-    changeNotesArgs.failOnLoadForTest.set(false);
-    atrScope.set(preDisableContext);
+    Context oldContext = atrScope.disableNoteDb();
+    return () -> {
+      changeNotesArgs.failOnLoadForTest.set(false);
+      atrScope.set(oldContext);
+    };
   }
 
   protected void disableChangeIndexWrites() {
@@ -827,55 +807,49 @@
 
   protected AutoCloseable disableChangeIndex() {
     disableChangeIndexWrites();
-    ChangeIndex searchIndex = changeIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledChangeIndex)) {
-      changeIndexes.setSearchIndex(new DisabledChangeIndex(searchIndex), false);
+    ChangeIndex maybeDisabledSearchIndex = changeIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledChangeIndex)) {
+      changeIndexes.setSearchIndex(new DisabledChangeIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() throws Exception {
-        enableChangeIndexWrites();
-        ChangeIndex searchIndex = changeIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledChangeIndex) {
-          changeIndexes.setSearchIndex(((DisabledChangeIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      enableChangeIndexWrites();
+      ChangeIndex maybeEnabledSearchIndex = changeIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledChangeIndex) {
+        changeIndexes.setSearchIndex(
+            ((DisabledChangeIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
 
   protected AutoCloseable disableAccountIndex() {
-    AccountIndex searchIndex = accountIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledAccountIndex)) {
-      accountIndexes.setSearchIndex(new DisabledAccountIndex(searchIndex), false);
+    AccountIndex maybeDisabledSearchIndex = accountIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledAccountIndex)) {
+      accountIndexes.setSearchIndex(new DisabledAccountIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() {
-        AccountIndex searchIndex = accountIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledAccountIndex) {
-          accountIndexes.setSearchIndex(((DisabledAccountIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      AccountIndex maybeEnabledSearchIndex = accountIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledAccountIndex) {
+        accountIndexes.setSearchIndex(
+            ((DisabledAccountIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
 
   protected AutoCloseable disableProjectIndex() {
     disableProjectIndexWrites();
-    ProjectIndex searchIndex = projectIndexes.getSearchIndex();
-    if (!(searchIndex instanceof DisabledProjectIndex)) {
-      projectIndexes.setSearchIndex(new DisabledProjectIndex(searchIndex), false);
+    ProjectIndex maybeDisabledSearchIndex = projectIndexes.getSearchIndex();
+    if (!(maybeDisabledSearchIndex instanceof DisabledProjectIndex)) {
+      projectIndexes.setSearchIndex(new DisabledProjectIndex(maybeDisabledSearchIndex), false);
     }
 
-    return new AutoCloseable() {
-      @Override
-      public void close() {
-        enableProjectIndexWrites();
-        ProjectIndex searchIndex = projectIndexes.getSearchIndex();
-        if (searchIndex instanceof DisabledProjectIndex) {
-          projectIndexes.setSearchIndex(((DisabledProjectIndex) searchIndex).unwrap(), false);
-        }
+    return () -> {
+      enableProjectIndexWrites();
+      ProjectIndex maybeEnabledSearchIndex = projectIndexes.getSearchIndex();
+      if (maybeEnabledSearchIndex instanceof DisabledProjectIndex) {
+        projectIndexes.setSearchIndex(
+            ((DisabledProjectIndex) maybeEnabledSearchIndex).unwrap(), false);
       }
     };
   }
@@ -916,6 +890,17 @@
     }
   }
 
+  protected void allowGlobalCapabilities(
+      AccountGroup.UUID id, int min, int max, String... capabilityNames) throws Exception {
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      for (String capabilityName : capabilityNames) {
+        Util.allow(
+            u.getConfig(), capabilityName, id, new PermissionRange(capabilityName, min, max));
+      }
+      u.save();
+    }
+  }
+
   protected void allowGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
       throws Exception {
     allowGlobalCapabilities(id, Arrays.asList(capabilityNames));
@@ -1075,7 +1060,7 @@
   }
 
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
   }
 
@@ -1101,12 +1086,12 @@
         .inOrder();
   }
 
-  protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
+  protected PatchSet getPatchSet(PatchSet.Id psId) {
     return changeDataFactory.create(project, psId.getParentKey()).patchSet(psId);
   }
 
   protected IdentifiedUser user(TestAccount testAccount) {
-    return identifiedUserFactory.create(testAccount.getId());
+    return identifiedUserFactory.create(testAccount.id());
   }
 
   protected RevisionResource parseCurrentRevisionResource(String changeId) throws Exception {
@@ -1216,13 +1201,6 @@
       throws Exception {
     TestRepository<?> localRepo = cloneProject(proj);
     GitUtil.fetch(localRepo, "refs/*:refs/*");
-    ImmutableMap<String, Ref> refs =
-        localRepo
-            .getRepository()
-            .getRefDatabase()
-            .getRefs()
-            .stream()
-            .collect(toImmutableMap(Ref::getName, r -> r));
     Map<Branch.NameKey, RevTree> refValues = new HashMap<>();
 
     for (Branch.NameKey b : trees.keySet()) {
@@ -1230,7 +1208,7 @@
         continue;
       }
 
-      Ref r = refs.get(b.get());
+      Ref r = localRepo.getRepository().exactRef(b.get());
       assertThat(r).isNotNull();
       RevWalk rw = localRepo.getRevWalk();
       RevCommit c = rw.parseCommit(r.getObjectId());
@@ -1377,7 +1355,7 @@
   }
 
   protected void assertNotifyTo(TestAccount expected) {
-    assertNotifyTo(expected.email, expected.fullName);
+    assertNotifyTo(expected.email(), expected.fullName());
   }
 
   protected void assertNotifyTo(String expectedEmail, String expectedFullname) {
@@ -1391,7 +1369,7 @@
   }
 
   protected void assertNotifyCc(TestAccount expected) {
-    assertNotifyCc(expected.emailAddress);
+    assertNotifyCc(expected.getEmailAddress());
   }
 
   protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
@@ -1411,7 +1389,7 @@
   protected void assertNotifyBcc(TestAccount expected) {
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(expected.emailAddress);
+    assertThat(m.rcpt()).containsExactly(expected.getEmailAddress());
     assertThat(m.headers().get("To").isEmpty()).isTrue();
     assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
@@ -1437,7 +1415,7 @@
   }
 
   protected void watch(PushOneCommit.Result r, ProjectWatchInfoConfiguration config)
-      throws OrmException, RestApiException {
+      throws RestApiException {
     watch(r.getChange().project().get(), config);
   }
 
@@ -1566,7 +1544,7 @@
     }
 
     public void save() throws Exception {
-      metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.getId()));
+      metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.id()));
       projectConfig.commit(metaDataUpdate);
       metaDataUpdate.close();
       metaDataUpdate = null;
@@ -1605,4 +1583,45 @@
     comments.sort(Comparator.comparing(c -> c.id));
     return comments;
   }
+
+  protected AutoCloseable installPlugin(String pluginName, Class<? extends Module> sysModuleClass)
+      throws Exception {
+    return installPlugin(pluginName, sysModuleClass, null, null);
+  }
+
+  protected AutoCloseable installPlugin(
+      String pluginName,
+      @Nullable Class<? extends Module> sysModuleClass,
+      @Nullable Class<? extends Module> httpModuleClass,
+      @Nullable Class<? extends Module> sshModuleClass)
+      throws Exception {
+    checkStatic(sysModuleClass);
+    checkStatic(httpModuleClass);
+    checkStatic(sshModuleClass);
+    TestServerPlugin plugin =
+        new TestServerPlugin(
+            pluginName,
+            "http://example.com/" + pluginName,
+            pluginUserFactory.create(pluginName),
+            getClass().getClassLoader(),
+            sysModuleClass != null ? sysModuleClass.getName() : null,
+            httpModuleClass != null ? httpModuleClass.getName() : null,
+            sshModuleClass != null ? sshModuleClass.getName() : null,
+            sitePaths.data_dir.resolve(pluginName));
+    plugin.start(pluginGuiceEnvironment);
+    pluginGuiceEnvironment.onStartPlugin(plugin);
+    return () -> {
+      plugin.stop(pluginGuiceEnvironment);
+      pluginGuiceEnvironment.onStopPlugin(plugin);
+    };
+  }
+
+  private static void checkStatic(@Nullable Class<? extends Module> moduleClass) {
+    if (moduleClass != null) {
+      checkArgument(
+          (moduleClass.getModifiers() & Modifier.STATIC) != 0,
+          "module must be static: %s",
+          moduleClass.getName());
+    }
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 4877f05..af2f17e 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -43,7 +43,6 @@
 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;
 import java.util.HashSet;
@@ -60,7 +59,7 @@
 
   @Before
   public void enableReviewerByEmail() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -86,7 +85,7 @@
     if (record) {
       accountsModifyingEmailStrategy.add(account);
     }
-    requestScopeOperations.setApiUser(account.getId());
+    requestScopeOperations.setApiUser(account.id());
     GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
     prefs.emailStrategy = strategy;
     gApi.accounts().self().setPreferences(prefs);
@@ -103,9 +102,10 @@
       super(failureMetadata, target);
     }
 
-    public FakeEmailSenderSubject notSent() {
-      if (actual().peekMessage() != null) {
-        failWithoutActual(fact("expected message", "sent"));
+    public FakeEmailSenderSubject didNotSend() {
+      Message message = actual().peekMessage();
+      if (message != null) {
+        failWithoutActual(fact("expected no message", message));
       }
       return this;
     }
@@ -120,9 +120,7 @@
       recipients.put(CC, parseAddresses(message, "Cc"));
       recipients.put(
           BCC,
-          message
-              .rcpt()
-              .stream()
+          message.rcpt().stream()
               .map(Address::getEmail)
               .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
               .collect(toList()));
@@ -133,7 +131,13 @@
       }
       EmailHeader header = message.headers().get("X-Gerrit-MessageType");
       if (!header.equals(new EmailHeader.String(messageType))) {
-        failWithoutActual(fact("expected message of type", messageType));
+        failWithoutActual(
+            fact("expected message of type", messageType),
+            fact(
+                "actual",
+                header instanceof EmailHeader.String
+                    ? ((EmailHeader.String) header).getString()
+                    : header));
       }
 
       // Return a named subject that displays a human-readable table of
@@ -209,7 +213,7 @@
 
     public FakeEmailSenderSubject noOneElse() {
       for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
-        if (!accountedFor.contains(watchEntry.getValue().email)) {
+        if (!accountedFor.contains(watchEntry.getValue().email())) {
           notTo(watchEntry.getKey());
         }
       }
@@ -262,7 +266,7 @@
     }
 
     private void rcpt(@Nullable RecipientType type, TestAccount account) {
-      rcpt(type, account.email);
+      rcpt(type, account.email());
     }
 
     public FakeEmailSenderSubject to(NotifyType... watches) {
@@ -330,8 +334,8 @@
       return description.getClassName();
     }
 
-    private TestAccount evictAndCopy(TestAccount account) throws IOException {
-      evictAndReindexAccount(account.id);
+    private TestAccount evictAndCopy(TestAccount account) {
+      evictAndReindexAccount(account.id());
       return account;
     }
 
@@ -360,7 +364,7 @@
         assignee = testAccount("assignee");
 
         watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
-        requestScopeOperations.setApiUser(watchingProjectOwner.getId());
+        requestScopeOperations.setApiUser(watchingProjectOwner.id());
         watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
 
         for (NotifyType watch : NotifyType.values()) {
@@ -368,7 +372,7 @@
             continue;
           }
           TestAccount watcher = testAccount(watch.toString());
-          requestScopeOperations.setApiUser(watcher.getId());
+          requestScopeOperations.setApiUser(watcher.id());
           watch(
               allProjects.get(),
               pwi -> {
@@ -399,20 +403,20 @@
     public TestAccount testAccount(String name) throws Exception {
       String username = name(name);
       TestAccount account = accountCreator.create(username, email(username), name);
-      accountsByEmail.put(account.email, account);
+      accountsByEmail.put(account.email(), account);
       return account;
     }
 
     public TestAccount testAccount(String name, String groupName) throws Exception {
       String username = name(name);
       TestAccount account = accountCreator.create(username, email(username), name, groupName);
-      accountsByEmail.put(account.email, account);
+      accountsByEmail.put(account.email(), account);
       return account;
     }
 
     String emailToName(String email) {
       if (accountsByEmail.containsKey(email)) {
-        return accountsByEmail.get(email).fullName;
+        return accountsByEmail.get(email).fullName();
       }
       return email;
     }
@@ -420,9 +424,9 @@
     protected void addReviewers(PushOneCommit.Result r) throws Exception {
       ReviewInput in =
           ReviewInput.noScore()
-              .reviewer(reviewer.email)
+              .reviewer(reviewer.email())
               .reviewer(reviewerByEmail)
-              .reviewer(ccer.email, ReviewerState.CC, false)
+              .reviewer(ccer.email(), ReviewerState.CC, false)
               .reviewer(ccerByEmail, ReviewerState.CC, false);
       ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
       supportReviewersByEmail = true;
@@ -430,8 +434,8 @@
         supportReviewersByEmail = false;
         in =
             ReviewInput.noScore()
-                .reviewer(reviewer.email)
-                .reviewer(ccer.email, ReviewerState.CC, false);
+                .reviewer(reviewer.email())
+                .reviewer(ccer.email(), ReviewerState.CC, false);
         result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
       }
       Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue();
@@ -461,9 +465,9 @@
       if (pushOptions != null) {
         ref = ref + '%' + Joiner.on(',').join(pushOptions);
       }
-      requestScopeOperations.setApiUser(owner.getId());
+      requestScopeOperations.setApiUser(owner.id());
       repo = cloneProject(project, owner);
-      PushOneCommit push = pushFactory.create(owner.getIdent(), repo);
+      PushOneCommit push = pushFactory.create(owner.newIdent(), repo);
       result = push.to(ref);
       result.assertOkStatus();
       changeId = result.getChangeId();
@@ -483,33 +487,38 @@
     StagedChange(String ref) throws Exception {
       super(ref);
 
-      requestScopeOperations.setApiUser(starrer.getId());
+      requestScopeOperations.setApiUser(starrer.id());
       gApi.accounts().self().starChange(result.getChangeId());
 
-      requestScopeOperations.setApiUser(owner.getId());
+      requestScopeOperations.setApiUser(owner.id());
       addReviewers(result);
       sender.clear();
     }
   }
 
   protected StagedChange stageReviewableChange() throws Exception {
-    return new StagedChange("refs/for/master");
+    StagedChange sc = new StagedChange("refs/for/master");
+    sender.clear();
+    return sc;
   }
 
   protected StagedChange stageWipChange() throws Exception {
-    return new StagedChange("refs/for/master%wip");
+    StagedChange sc = new StagedChange("refs/for/master%wip");
+    sender.clear();
+    return sc;
   }
 
   protected StagedChange stageReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).setWorkInProgress();
+    sender.clear();
     return sc;
   }
 
   protected StagedChange stageAbandonedReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -517,7 +526,7 @@
 
   protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChange();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -525,7 +534,7 @@
 
   protected StagedChange stageAbandonedWipChange() throws Exception {
     StagedChange sc = stageWipChange();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
new file mode 100644
index 0000000..ccd30ab
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -0,0 +1,201 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.restapi.change.GetChange;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import com.google.gerrit.sshd.commands.Query;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.util.List;
+import java.util.Objects;
+import org.kohsuke.args4j.Option;
+
+public class AbstractPluginFieldsTest extends AbstractDaemonTest {
+  protected static class MyInfo extends PluginDefinedInfo {
+    @Nullable String theAttribute;
+
+    public MyInfo(@Nullable String theAttribute) {
+      this.theAttribute = theAttribute;
+    }
+
+    MyInfo(String name, @Nullable String theAttribute) {
+      this.name = requireNonNull(name);
+      this.theAttribute = theAttribute;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof MyInfo)) {
+        return false;
+      }
+      MyInfo i = (MyInfo) o;
+      return Objects.equals(name, i.name) && Objects.equals(theAttribute, i.theAttribute);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, theAttribute);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("name", name)
+          .add("theAttribute", theAttribute)
+          .toString();
+    }
+  }
+
+  protected static class NullAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class).toInstance((cd, bp, p) -> null);
+    }
+  }
+
+  protected static class SimpleAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
+          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
+    }
+  }
+
+  private static class MyOptions implements DynamicBean {
+    @Option(name = "--opt")
+    private String opt;
+  }
+
+  protected static class OptionAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
+          .toInstance(
+              (cd, bp, p) -> {
+                MyOptions opts = (MyOptions) bp.getDynamicBean(p);
+                return opts != null ? new MyInfo("opt " + opts.opt) : null;
+              });
+      bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
+      bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
+      bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
+    }
+  }
+
+  protected void getChangeWithNullAttribute(PluginInfoGetter getter) throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getter.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", NullAttributeModule.class)) {
+      assertThat(getter.call(id)).isNull();
+    }
+
+    assertThat(getter.call(id)).isNull();
+  }
+
+  protected void getChangeWithSimpleAttribute(PluginInfoGetter getter) throws Exception {
+    getChangeWithSimpleAttribute(getter, SimpleAttributeModule.class);
+  }
+
+  protected void getChangeWithSimpleAttribute(
+      PluginInfoGetter getter, Class<? extends Module> moduleClass) throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getter.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", moduleClass)) {
+      assertThat(getter.call(id)).containsExactly(new MyInfo("my-plugin", "change " + id));
+    }
+
+    assertThat(getter.call(id)).isNull();
+  }
+
+  protected void getChangeWithOption(
+      PluginInfoGetter getterWithoutOptions, PluginInfoGetterWithOptions getterWithOptions)
+      throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getterWithoutOptions.call(id)).isNull();
+
+    try (AutoCloseable ignored = installPlugin("my-plugin", OptionAttributeModule.class)) {
+      assertThat(getterWithoutOptions.call(id))
+          .containsExactly(new MyInfo("my-plugin", "opt null"));
+      assertThat(getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")))
+          .containsExactly(new MyInfo("my-plugin", "opt foo"));
+    }
+
+    assertThat(getterWithoutOptions.call(id)).isNull();
+  }
+
+  protected static List<MyInfo> pluginInfoFromSingletonList(List<ChangeInfo> changeInfos) {
+    assertThat(changeInfos).hasSize(1);
+    return pluginInfoFromChangeInfo(changeInfos.get(0));
+  }
+
+  protected static List<MyInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
+    List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
+    if (pluginInfo == null) {
+      return null;
+    }
+    return pluginInfo.stream().map(MyInfo.class::cast).collect(toImmutableList());
+  }
+
+  /**
+   * Decode {@code MyInfo}s from a raw list of maps returned from Gson.
+   *
+   * <p>This method is used instead of decoding {@code ChangeInfo} or {@code ChangAttribute}, since
+   * Gson would decode the {@code plugins} field as a {@code List<PluginDefinedInfo>}, which would
+   * return the base type and silently ignore any fields that are defined only in the subclass.
+   * Instead, decode the enclosing {@code ChangeInfo} or {@code ChangeAttribute} as a raw {@code
+   * Map<String, Object>}, and pass the {@code "plugins"} value to this method.
+   *
+   * @param gson Gson converter.
+   * @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
+   * @return decoded list of {@code MyInfo}s.
+   */
+  protected static List<MyInfo> decodeRawPluginsList(Gson gson, @Nullable Object plugins) {
+    if (plugins == null) {
+      return null;
+    }
+    checkArgument(plugins instanceof List, "not a list: %s", plugins);
+    return gson.fromJson(gson.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+  }
+
+  @FunctionalInterface
+  protected interface PluginInfoGetter {
+    List<MyInfo> call(Change.Id id) throws Exception;
+  }
+
+  @FunctionalInterface
+  protected interface PluginInfoGetterWithOptions {
+    List<MyInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
+        throws Exception;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 8420ff2..50536d8 100644
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -145,7 +145,10 @@
     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();
     Context ctx = new Context(old.session, old.user, old.created);
 
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index b8a8776..aeae2c2 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -20,10 +20,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.GroupCache;
@@ -31,7 +30,7 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -108,7 +107,7 @@
       }
     }
 
-    account = new TestAccount(id, username, email, fullName, httpPass);
+    account = TestAccount.create(id, username, email, fullName, httpPass);
     if (username != null) {
       accounts.put(username, account);
     }
@@ -149,7 +148,7 @@
   }
 
   public void evict(Collection<Account.Id> ids) {
-    accounts.values().removeIf(a -> ids.contains(a.id));
+    accounts.values().removeIf(a -> ids.contains(a.id()));
   }
 
   public ImmutableList<TestAccount> getAll() {
@@ -157,7 +156,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index be7995f..ed0de9c 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -1,4 +1,5 @@
 load("//tools/bzl:java.bzl", "java_library2")
+load("//tools/bzl:javadoc.bzl", "java_doc")
 
 java_library(
     name = "lib",
@@ -36,7 +37,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib:jimfs",
         "//lib:jsch",
@@ -68,6 +68,7 @@
     testonly = True,
     srcs = glob(["**/*.java"]),
     exported_deps = [
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/httpd/auth/openid",
         "//java/com/google/gerrit/index:query_exception",
@@ -109,13 +110,15 @@
         "//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",
@@ -129,8 +132,6 @@
     ],
 )
 
-load("//tools/bzl:javadoc.bzl", "java_doc")
-
 java_doc(
     name = "framework-javadoc",
     testonly = True,
diff --git a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 0aa56cf..0a1d765 100644
--- a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.auto.value.AutoAnnotation;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import java.lang.annotation.Annotation;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -45,32 +45,13 @@
     return cfg;
   }
 
-  static class GlobalPluginConfigToGerritConfig implements GerritConfig {
-    private final GlobalPluginConfig delegate;
+  private static GerritConfig toGerritConfig(GlobalPluginConfig annotation) {
+    return newGerritConfig(annotation.name(), annotation.value(), annotation.values());
+  }
 
-    GlobalPluginConfigToGerritConfig(GlobalPluginConfig delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return delegate.annotationType();
-    }
-
-    @Override
-    public String name() {
-      return delegate.name();
-    }
-
-    @Override
-    public String value() {
-      return delegate.value();
-    }
-
-    @Override
-    public String[] values() {
-      return delegate.values();
-    }
+  @AutoAnnotation
+  private static GerritConfig newGerritConfig(String name, String value, String[] values) {
+    return new AutoAnnotation_ConfigAnnotationParser_newGerritConfig(name, value, values);
   }
 
   static Map<String, Config> parse(GlobalPluginConfig annotation) {
@@ -79,7 +60,7 @@
     }
     Map<String, Config> result = new HashMap<>();
     Config cfg = new Config();
-    parseAnnotation(cfg, new GlobalPluginConfigToGerritConfig(annotation));
+    parseAnnotation(cfg, toGerritConfig(annotation));
     result.put(annotation.pluginName(), cfg);
     return result;
   }
@@ -100,7 +81,7 @@
         config = new Config();
         result.put(pluginName, config);
       }
-      parseAnnotation(config, new GlobalPluginConfigToGerritConfig(c));
+      parseAnnotation(config, toGerritConfig(c));
     }
 
     return result;
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 0d473af..a32c6d1 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -20,10 +20,8 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.io.IOException;
 import java.util.Optional;
 
 /**
@@ -54,17 +52,17 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
@@ -75,12 +73,12 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 
   @Override
-  public Optional<ChangeData> get(Change.Id key, QueryOptions opts) throws IOException {
+  public Optional<ChangeData> get(Change.Id key, QueryOptions opts) {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
   }
 }
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 1af71b8..6c6e1bf 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -55,7 +55,7 @@
     }
 
     public EventRecorder create(TestAccount user) {
-      return new EventRecorder(eventListeners, userFactory.create(user.id));
+      return new EventRecorder(eventListeners, userFactory.create(user.id()));
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
new file mode 100644
index 0000000..48dc408
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.util.Comparator.comparing;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.httpd.GitOverHttpServlet;
+import com.google.gerrit.server.AuditEvent;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.gerrit.server.audit.AuditService;
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.gerrit.server.audit.group.GroupAuditListener;
+import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Singleton
+public class FakeGroupAuditService extends AuditService {
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.setOf(binder(), GroupAuditListener.class);
+      DynamicSet.setOf(binder(), AuditListener.class);
+
+      // Use this fake service at the Guice level rather than depending on tests binding their own
+      // audit listeners. If we used per-test listeners, then there would be a race between
+      // dispatching the audit events from HTTP requests performed during test setup in
+      // AbstractDaemonTest, and the later test setup binding the audit listener. Using a separate
+      // audit service implementation ensures all events get recorded.
+      bind(GroupAuditService.class).to(FakeGroupAuditService.class);
+    }
+  }
+
+  private final GitOverHttpServlet.Metrics httpMetrics;
+  private final BlockingQueue<HttpAuditEvent> httpEvents;
+  private final AtomicLong drainedSoFar;
+
+  @Inject
+  FakeGroupAuditService(
+      PluginSetContext<AuditListener> auditListeners,
+      PluginSetContext<GroupAuditListener> groupAuditListeners,
+      GitOverHttpServlet.Metrics httpMetrics) {
+    super(auditListeners, groupAuditListeners);
+    this.httpMetrics = httpMetrics;
+    this.httpEvents = new LinkedBlockingQueue<>();
+    this.drainedSoFar = new AtomicLong();
+  }
+
+  @Override
+  public void dispatch(AuditEvent action) {
+    super.dispatch(action);
+    if (action instanceof HttpAuditEvent) {
+      httpEvents.add((HttpAuditEvent) action);
+    }
+  }
+
+  public ImmutableList<HttpAuditEvent> drainHttpAuditEvents() throws Exception {
+    // Assumes that all HttpAuditEvents are produced by GitOverHttpServlet.
+    int expectedSize = Ints.checkedCast(httpMetrics.getRequestsStarted() - drainedSoFar.get());
+    List<HttpAuditEvent> result = new ArrayList<>();
+    for (int i = 0; i < expectedSize; i++) {
+      HttpAuditEvent e = httpEvents.poll(30, SECONDS);
+      if (e == null) {
+        throw new AssertionError(
+            String.format("Timeout after receiving %d/%d audit events", i, expectedSize));
+      }
+      drainedSoFar.incrementAndGet();
+      result.add(e);
+    }
+    return ImmutableList.sortedCopyOf(comparing(e -> e.when), result);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index f829842..3a49f46 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -47,7 +47,6 @@
 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.InMemoryRepositoryManager;
 import com.google.gerrit.testing.SshMode;
 import com.google.inject.AbstractModule;
@@ -109,11 +108,13 @@
           !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.
           null, // @GlobalPluginConfig is only valid on methods.
-          null); // @GlobalPluginConfigs is only valid on methods.
+          null, // @GlobalPluginConfigs is only valid on methods.
+          getLogLevelThresholdAnnotation(testDesc));
     }
 
     public static Description forTestMethod(
@@ -128,12 +129,15 @@
               && !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),
           testDesc.getAnnotation(GerritConfigs.class),
           testDesc.getAnnotation(GlobalPluginConfig.class),
-          testDesc.getAnnotation(GlobalPluginConfigs.class));
+          testDesc.getAnnotation(GlobalPluginConfigs.class),
+          getLogLevelThresholdAnnotation(testDesc));
     }
 
     private static boolean has(Class<? extends Annotation> annotation, Class<?> clazz) {
@@ -145,6 +149,14 @@
       return false;
     }
 
+    private static Level getLogLevelThresholdAnnotation(org.junit.runner.Description testDesc) {
+      LogThreshold logLevelThreshold = testDesc.getTestClass().getAnnotation(LogThreshold.class);
+      if (logLevelThreshold == null) {
+        return Level.DEBUG;
+      }
+      return Level.toLevel(logLevelThreshold.level());
+    }
+
     abstract org.junit.runner.Description testDescription();
 
     @Nullable
@@ -156,6 +168,8 @@
 
     abstract boolean sandboxed();
 
+    abstract boolean skipProjectClone();
+
     abstract boolean useSshAnnotation();
 
     boolean useSsh() {
@@ -174,6 +188,8 @@
     @Nullable
     abstract GlobalPluginConfigs pluginConfigs();
 
+    abstract Level logLevelThreshold();
+
     private void checkValidAnnotations() {
       if (configs() != null && config() != null) {
         throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
@@ -360,7 +376,7 @@
       throws Exception {
     checkArgument(site != null, "site is required (even for in-memory server");
     desc.checkValidAnnotations();
-    configureLogging();
+    configureLogging(desc.logLevelThreshold());
     CyclicBarrier serverStarted = new CyclicBarrier(2);
     Daemon daemon =
         new Daemon(
@@ -374,7 +390,9 @@
             site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
     daemon.setAuditEventModuleForTesting(new FakeGroupAuditService.Module());
-    daemon.setAdditionalSysModuleForTesting(testSysModule);
+    if (testSysModule != null) {
+      daemon.addAdditionalSysModuleForTesting(testSysModule);
+    }
     daemon.setEnableSshd(desc.useSsh());
 
     if (desc.memory()) {
@@ -415,6 +433,8 @@
                 bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
               }
             }));
+    daemon.addAdditionalSysModuleForTesting(
+        new ReindexProjectsAtStartup.Module(), new ReindexGroupsAtStartup.Module());
     daemon.start();
     return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
   }
@@ -460,7 +480,7 @@
     return new GerritServer(desc, site, createTestInjector(daemon), daemon, daemonService);
   }
 
-  private static void configureLogging() {
+  private static void configureLogging(Level threshold) {
     LogManager.resetConfiguration();
 
     PatternLayout layout = new PatternLayout();
@@ -469,7 +489,7 @@
     ConsoleAppender dst = new ConsoleAppender();
     dst.setLayout(layout);
     dst.setTarget("System.err");
-    dst.setThreshold(Level.DEBUG);
+    dst.setThreshold(threshold);
     dst.activateOptions();
 
     Logger root = LogManager.getRootLogger();
diff --git a/java/com/google/gerrit/acceptance/HttpResponse.java b/java/com/google/gerrit/acceptance/HttpResponse.java
index 8132c32..88079a4 100644
--- a/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -65,8 +65,7 @@
   }
 
   public ImmutableList<String> getHeaders(String name) {
-    return Arrays.asList(response.getHeaders(name))
-        .stream()
+    return Arrays.asList(response.getHeaders(name)).stream()
         .map(Header::getValue)
         .collect(toImmutableList());
   }
diff --git a/java/com/google/gerrit/acceptance/HttpSession.java b/java/com/google/gerrit/acceptance/HttpSession.java
index fdd1fce..833c53b 100644
--- a/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/java/com/google/gerrit/acceptance/HttpSession.java
@@ -37,7 +37,7 @@
     this.account = account;
     if (account != null) {
       executor.auth(
-          new HttpHost(uri.getHost(), uri.getPort()), account.username, account.httpPassword);
+          new HttpHost(uri.getHost(), uri.getPort()), account.username(), account.httpPassword());
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 844d6c8..a704d2f 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -17,6 +17,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -30,8 +31,6 @@
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
 import java.io.IOException;
@@ -91,8 +90,8 @@
     public void start() {
       try {
         schemaCreator.ensureCreated();
-      } catch (OrmException | IOException | ConfigInvalidException e) {
-        throw new OrmRuntimeException(e);
+      } catch (IOException | ConfigInvalidException e) {
+        throw new StorageException(e);
       }
     }
 
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java b/java/com/google/gerrit/acceptance/LogThreshold.java
similarity index 63%
copy from java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
copy to java/com/google/gerrit/acceptance/LogThreshold.java
index 336edeb..36831f3 100644
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
+++ b/java/com/google/gerrit/acceptance/LogThreshold.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -11,14 +11,19 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
 
-package com.google.gerrit.server.config;
-
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
 
+@Target({TYPE, METHOD})
 @Retention(RUNTIME)
-@BindingAnnotation
-public @interface DisableReverseDnsLookup {}
+@Inherited
+public @interface LogThreshold {
+  String level() default "DEBUG";
+}
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 7d0a59c..ae397a9 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -260,9 +260,7 @@
         refsPatternByProject.asMap().entrySet()) {
       try (Repository repo = repoManager.openRepository(e.getKey())) {
         Collection<Ref> nonRestoredRefs =
-            repo.getRefDatabase()
-                .getRefs()
-                .stream()
+            repo.getRefDatabase().getRefs().stream()
                 .filter(
                     r ->
                         !keptRefsByProject.containsEntry(e.getKey(), r.getName())
@@ -315,9 +313,7 @@
 
   private Set<Project.NameKey> projectsWithConfigChanges(
       Multimap<Project.NameKey, String> projects) {
-    return projects
-        .entries()
-        .stream()
+    return projects.entries().stream()
         .filter(e -> e.getValue().equals(RefNames.REFS_CONFIG))
         .map(Map.Entry::getKey)
         .collect(toSet());
@@ -357,7 +353,7 @@
   }
 
   /** Evict groups that were modified. */
-  private void evictAndReindexGroups() throws IOException {
+  private void evictAndReindexGroups() {
     if (groupCache != null || groupIndexer != null) {
       Set<AccountGroup.UUID> modifiedGroups =
           new HashSet<>(groupUUIDs(restoredRefsByProject.get(allUsersName)));
@@ -371,7 +367,7 @@
     }
   }
 
-  private void evictAndReindexAccount(Account.Id accountId) throws IOException {
+  private void evictAndReindexAccount(Account.Id accountId) {
     if (accountCache != null) {
       accountCache.evict(accountId);
     }
@@ -383,7 +379,7 @@
     }
   }
 
-  private void evictAndReindexGroup(AccountGroup.UUID uuid) throws IOException {
+  private void evictAndReindexGroup(AccountGroup.UUID uuid) {
     if (groupCache != null) {
       groupCache.evict(uuid);
     }
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index f16f1d3..e15dd40 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -334,15 +333,15 @@
       this.resSubj = subject;
     }
 
-    public ChangeData getChange() throws OrmException {
+    public ChangeData getChange() {
       return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
     }
 
-    public PatchSet getPatchSet() throws OrmException {
+    public PatchSet getPatchSet() {
       return getChange().currentPatchSet();
     }
 
-    public PatchSet.Id getPatchSetId() throws OrmException {
+    public PatchSet.Id getPatchSetId() {
       return getChange().change().currentPatchSetId();
     }
 
@@ -359,8 +358,7 @@
     }
 
     public void assertChange(
-        Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers)
-        throws OrmException {
+        Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) {
       assertChange(
           expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of());
     }
@@ -369,8 +367,7 @@
         Change.Status expectedStatus,
         String expectedTopic,
         List<TestAccount> expectedReviewers,
-        List<TestAccount> expectedCcs)
-        throws OrmException {
+        List<TestAccount> expectedCcs) {
       Change c = getChange().change();
       assertThat(c.getSubject()).isEqualTo(resSubj);
       assertThat(c.getStatus()).isEqualTo(expectedStatus);
@@ -380,8 +377,7 @@
     }
 
     private void assertReviewers(
-        Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers)
-        throws OrmException {
+        Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) {
       Iterable<Account.Id> actualIds =
           approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state);
       assertThat(actualIds)
diff --git a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
index 5209f90..19910db 100644
--- a/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -19,10 +19,9 @@
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.io.IOException;
 
 class ReadOnlyChangeIndex implements ChangeIndex {
   private final ChangeIndex index;
@@ -46,17 +45,17 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     // do nothing
   }
 
   @Override
-  public void delete(Id key) throws IOException {
+  public void delete(Change.Id key) {
     // do nothing
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     // do nothing
   }
 
@@ -67,7 +66,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     // do nothing
   }
 }
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
new file mode 100644
index 0000000..bd8a926
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.data.GroupReference;
+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.group.db.Groups;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/** Reindex all groups at Gerrit daemon startup. */
+public class ReindexGroupsAtStartup implements LifecycleListener {
+  private final GroupIndexer groupIndexer;
+  private final Groups groups;
+  private final Config cfg;
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      listener().to(ReindexGroupsAtStartup.class).in(Scopes.SINGLETON);
+    }
+  }
+
+  @Inject
+  public ReindexGroupsAtStartup(
+      GroupIndexer groupIndexer, Groups groups, @GerritServerConfig Config cfg) {
+    this.groupIndexer = groupIndexer;
+    this.groups = groups;
+    this.cfg = cfg;
+  }
+
+  @Override
+  public void start() {
+    // Gerrit slaves without a reindex
+    if (cfg.getBoolean("container", "slave", false)
+        && !cfg.getBoolean("index", "scheduledIndexer", "runOnStartup", true)) {
+      return;
+    }
+
+    Stream<GroupReference> allGroupReferences;
+    try {
+      allGroupReferences = groups.getAllGroupReferences();
+    } catch (ConfigInvalidException | IOException e) {
+      throw new IllegalStateException("Unable to reindex groups, tests may fail", e);
+    }
+
+    allGroupReferences.forEach(group -> groupIndexer.index(group.getUUID()));
+  }
+
+  @Override
+  public void stop() {}
+}
diff --git a/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
new file mode 100644
index 0000000..2f0ffcb
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+
+/** Reindex all projects at Gerrit daemon startup. */
+public class ReindexProjectsAtStartup implements LifecycleListener {
+  private final ProjectIndexer projectIndexer;
+  private final GitRepositoryManager repoMgr;
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      listener().to(ReindexProjectsAtStartup.class).in(Scopes.SINGLETON);
+    }
+  }
+
+  @Inject
+  public ReindexProjectsAtStartup(ProjectIndexer projectIndexer, GitRepositoryManager repoMgr) {
+    this.projectIndexer = projectIndexer;
+    this.repoMgr = repoMgr;
+  }
+
+  @Override
+  public void start() {
+    repoMgr.list().stream().forEach(projectIndexer::index);
+  }
+
+  @Override
+  public void stop() {}
+}
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java b/java/com/google/gerrit/acceptance/SkipProjectClone.java
similarity index 68%
copy from java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
copy to java/com/google/gerrit/acceptance/SkipProjectClone.java
index 336edeb..9a326d8 100644
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
+++ b/java/com/google/gerrit/acceptance/SkipProjectClone.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+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 com.google.inject.BindingAnnotation;
 import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
 
+@Target({TYPE, METHOD})
 @Retention(RUNTIME)
-@BindingAnnotation
-public @interface DisableReverseDnsLookup {}
+public @interface SkipProjectClone {}
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index db93009..29d0b35 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -59,7 +59,7 @@
       this.server = server;
       Injector i = server.getTestInjector();
       if (adminId == null) {
-        adminId = i.getInstance(AccountCreator.class).admin().getId();
+        adminId = i.getInstance(AccountCreator.class).admin().id();
       }
       ctx = i.getInstance(OneOffRequestContext.class).openAs(adminId);
       GerritApi gApi = i.getInstance(GerritApi.class);
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 5ce44ff..c937aed 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -14,61 +14,80 @@
 
 package com.google.gerrit.acceptance;
 
-import static java.util.stream.Collectors.toList;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.common.net.InetAddresses;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
-import java.util.List;
 import org.apache.http.client.utils.URIBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 
-public class TestAccount {
-  public static List<Account.Id> ids(List<TestAccount> accounts) {
-    return accounts.stream().map(a -> a.id).collect(toList());
+@AutoValue
+public abstract class TestAccount {
+  public static ImmutableList<Account.Id> ids(Iterable<TestAccount> accounts) {
+    return Streams.stream(accounts).map(TestAccount::id).collect(toImmutableList());
   }
 
-  public static List<String> names(List<TestAccount> accounts) {
-    return accounts.stream().map(a -> a.fullName).collect(toList());
+  public static ImmutableList<String> names(Iterable<TestAccount> accounts) {
+    return Streams.stream(accounts).map(TestAccount::fullName).collect(toImmutableList());
   }
 
-  public static List<String> names(TestAccount... accounts) {
+  public static ImmutableList<String> names(TestAccount... accounts) {
     return names(Arrays.asList(accounts));
   }
 
-  public final Account.Id id;
-  public final String username;
-  public final String email;
-  public final Address emailAddress;
-  public final String fullName;
-  public final String httpPassword;
-
-  TestAccount(Account.Id id, String username, String email, String fullName, String httpPassword) {
-    this.id = id;
-    this.username = username;
-    this.email = email;
-    this.emailAddress = new Address(fullName, email);
-    this.fullName = fullName;
-    this.httpPassword = httpPassword;
+  static TestAccount create(
+      Account.Id id,
+      @Nullable String username,
+      @Nullable String email,
+      @Nullable String fullName,
+      @Nullable String httpPassword) {
+    return new AutoValue_TestAccount(id, username, email, fullName, httpPassword);
   }
 
-  public PersonIdent getIdent() {
-    return new PersonIdent(fullName, email);
+  public abstract Account.Id id();
+
+  @Nullable
+  public abstract String username();
+
+  @Nullable
+  public abstract String email();
+
+  @Nullable
+  public abstract String fullName();
+
+  @Nullable
+  public abstract String httpPassword();
+
+  public PersonIdent newIdent() {
+    return new PersonIdent(fullName(), email());
   }
 
   public String getHttpUrl(GerritServer server) {
     InetSocketAddress addr = server.getHttpAddress();
     return new URIBuilder()
         .setScheme("http")
-        .setUserInfo(username, httpPassword)
+        .setUserInfo(username(), httpPassword())
         .setHost(InetAddresses.toUriString(addr.getAddress()))
         .setPort(addr.getPort())
         .toString();
   }
 
-  public Account.Id getId() {
-    return id;
+  public Address getEmailAddress() {
+    // Address is weird enough that it's safer and clearer to create a new instance in a
+    // non-abstract method rather than, say, having an abstract emailAddress() as part of this
+    // AutoValue class. Specifically:
+    //  * Email is not specified as @Nullable in Address, but it is nullable in this class. If this
+    //    is a problem, at least it's a problem only for users of TestAccount that actually call
+    //    emailAddress().
+    //  * Address#equals only considers email, not name, whereas TestAccount#equals should include
+    //    name.
+    return new Address(fullName(), email());
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index 7f55c97..7641e47 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -17,14 +17,13 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.InternalAccountUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
@@ -68,7 +67,7 @@
   }
 
   private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     Account.Id accountId = new Account.Id(seq.nextAccountId());
     return accountsUpdate.insert("Create Test Account", accountId, accountUpdater);
   }
@@ -146,7 +145,7 @@
     }
 
     private void updateAccount(TestAccountUpdate accountUpdate)
-        throws OrmException, IOException, ConfigInvalidException {
+        throws IOException, ConfigInvalidException {
       AccountsUpdate.AccountUpdater accountUpdater =
           (account, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountId);
       Optional<AccountState> updatedAccount = updateAccount(accountUpdater);
@@ -154,7 +153,7 @@
     }
 
     private Optional<AccountState> updateAccount(AccountsUpdate.AccountUpdater accountUpdater)
-        throws OrmException, IOException, ConfigInvalidException {
+        throws IOException, ConfigInvalidException {
       return accountsUpdate.update("Update Test Account", accountId, accountUpdater);
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
index 0cb5cf3..4847fdb 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -55,14 +55,14 @@
   public KeyPair getKeyPair(com.google.gerrit.acceptance.TestAccount account) throws Exception {
     checkState(sshEnabled, "Requested SSH key pair, but SSH is disabled");
     checkState(
-        account.username != null,
+        account.username() != null,
         "Requested SSH key pair for account %s, but username is not set",
-        account.id);
+        account.id());
 
-    String username = account.username;
+    String username = account.username();
     KeyPair keyPair = sshKeyPairs.get(username);
     if (keyPair == null) {
-      keyPair = createKeyPair(account.id, username, account.email);
+      keyPair = createKeyPair(account.id(), username, account.email());
       sshKeyPairs.put(username, keyPair);
     }
     return keyPair;
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
index 7ce5afa..e0ddee5 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
@@ -16,10 +16,10 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.group.InternalGroup;
@@ -27,8 +27,7 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
@@ -70,7 +69,7 @@
   }
 
   private AccountGroup.UUID createNewGroup(TestGroupCreation groupCreation)
-      throws ConfigInvalidException, IOException, OrmException {
+      throws ConfigInvalidException, IOException {
     InternalGroupCreation internalGroupCreation = toInternalGroupCreation(groupCreation);
     InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupCreation);
     InternalGroup internalGroup =
@@ -78,8 +77,7 @@
     return internalGroup.getGroupUUID();
   }
 
-  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation)
-      throws OrmException {
+  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation) {
     AccountGroup.Id groupId = new AccountGroup.Id(seq.nextGroupId());
     String groupName = groupCreation.name().orElse("group-with-id-" + groupId.get());
     AccountGroup.UUID groupUuid = GroupUUID.make(groupName, serverIdent);
@@ -148,7 +146,7 @@
     }
 
     private void updateGroup(TestGroupUpdate groupUpdate)
-        throws OrmDuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
+        throws DuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
       InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupUpdate);
       groupsUpdate.updateGroup(groupUuid, internalGroupUpdate);
     }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 1f2fa68..28be3f3 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -17,7 +17,6 @@
 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.Project.NameKey;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.CreateProjectArgs;
@@ -63,7 +62,7 @@
   }
 
   @Override
-  public ProjectOperations.PerProjectOperations project(NameKey key) {
+  public ProjectOperations.PerProjectOperations project(Project.NameKey key) {
     return new PerProjectOperations(key);
   }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index a63d28a..17d9294 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -28,9 +28,9 @@
    * 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 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.
+   * 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.
@@ -41,9 +41,9 @@
    * 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 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.
+   * 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.
@@ -68,4 +68,11 @@
    * @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
index 27b71b9..5546422 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -29,6 +29,7 @@
 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;
@@ -48,6 +49,7 @@
   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;
 
@@ -58,6 +60,7 @@
       AccountOperations accountOperations,
       GenericFactory userFactory,
       Provider<AnonymousUser> anonymousUserProvider,
+      InternalUser.Factory internalUserFactory,
       @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
       TestSshKeys testSshKeys) {
     this.atrScope = atrScope;
@@ -65,6 +68,7 @@
     this.accountOperations = accountOperations;
     this.userFactory = userFactory;
     this.anonymousUserProvider = anonymousUserProvider;
+    this.internalUserFactory = internalUserFactory;
     this.sshAddress = sshAddress;
     this.testSshKeys = testSshKeys;
   }
@@ -95,6 +99,11 @@
     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
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 9ceea39..a434c34 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -1,15 +1,13 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
-
 ANNOTATIONS = [
     "Nullable.java",
-    "audit/Audit.java",
-    "auth/SignInRequired.java",
+    "UsedAt.java",
 ]
 
 java_library(
     name = "annotations",
     srcs = ANNOTATIONS,
     visibility = ["//visibility:public"],
+    deps = ["//lib:guava"],
 )
 
 java_library(
@@ -24,9 +22,9 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gwtorm",
         "//java/org/eclipse/jgit:server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/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/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 bfd5ef9..0000000
--- a/java/com/google/gerrit/common/ProjectUtil.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.common;
-
-public class ProjectUtil {
-  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);
-      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 be8c16e..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();
 
diff --git a/java/com/google/gerrit/server/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
similarity index 89%
rename from java/com/google/gerrit/server/UsedAt.java
rename to java/com/google/gerrit/common/UsedAt.java
index b564157..1be6353 100644
--- a/java/com/google/gerrit/server/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -12,14 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+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 com.google.common.annotations.GwtCompatible;
-import com.google.inject.BindingAnnotation;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
@@ -27,14 +25,13 @@
  * 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_CHECKS,
     PLUGIN_DELETE_PROJECT,
     PLUGIN_SERVICEUSER,
     PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
diff --git a/java/com/google/gerrit/common/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 b3da199..3670e96 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -25,16 +25,34 @@
 import java.util.Set;
 
 /** Portion of a {@link Project} describing access rules. */
-public class AccessSection extends RefConfigSection implements Comparable<AccessSection> {
+public final class AccessSection implements Comparable<AccessSection> {
   /** Special name given to the global capabilities; not a valid reference. */
   public static final String GLOBAL_CAPABILITIES = "GLOBAL_CAPABILITIES";
+  /** Pattern that matches all references in a project. */
+  public static final String ALL = "refs/*";
 
-  protected List<Permission> permissions;
+  /** Pattern that matches all branches in a project. */
+  public static final String HEADS = "refs/heads/*";
 
-  protected AccessSection() {}
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
 
-  public AccessSection(String refPattern) {
-    super(refPattern);
+  /** Name of the access section. It could be a ref pattern or something else. */
+  private String name;
+
+  private List<Permission> permissions;
+
+  public AccessSection(String name) {
+    this.name = name;
+  }
+
+  /** @return true if the name is likely to be a valid reference section name. */
+  public static boolean isValidRefSectionName(String name) {
+    return name.startsWith("refs/") || name.startsWith("^refs/");
+  }
+
+  public String getName() {
+    return name;
   }
 
   public ImmutableList<Permission> getPermissions() {
@@ -145,7 +163,12 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (!super.equals(obj) || !(obj instanceof AccessSection)) {
+    if (!(obj instanceof AccessSection)) {
+      return false;
+    }
+
+    AccessSection other = (AccessSection) obj;
+    if (!getName().equals(other.getName())) {
       return false;
     }
     return new HashSet<>(getPermissions())
@@ -160,6 +183,7 @@
         hashCode += permission.hashCode();
       }
     }
+    hashCode += getName().hashCode();
     return hashCode;
   }
 }
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index 2e9c2d6..3ba0ba7 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -46,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;
@@ -78,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);
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 a40af22..0000000
--- a/java/com/google/gerrit/common/data/ProjectAccess.java
+++ /dev/null
@@ -1,143 +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.extensions.common.WebLinkInfo;
-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<WebLinkInfo> 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<WebLinkInfo> links) {
-    fileHistoryLinks = links;
-  }
-
-  public List<WebLinkInfo> getFileHistoryLinks() {
-    return fileHistoryLinks;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/RefConfigSection.java b/java/com/google/gerrit/common/data/RefConfigSection.java
deleted file mode 100644
index 663379a..0000000
--- a/java/com/google/gerrit/common/data/RefConfigSection.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-public abstract class RefConfigSection {
-  /** Pattern that matches all references in a project. */
-  public static final String ALL = "refs/*";
-
-  /** Pattern that matches all branches in a project. */
-  public static final String HEADS = "refs/heads/*";
-
-  /** Prefix that triggers a regular expression pattern. */
-  public static final String REGEX_PREFIX = "^";
-
-  /** @return true if the name is likely to be a valid reference section name. */
-  public static boolean isValid(String name) {
-    return name.startsWith("refs/") || name.startsWith("^refs/");
-  }
-
-  protected String name;
-
-  public RefConfigSection() {}
-
-  public RefConfigSection(String name) {
-    setName(name);
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String name) {
-    this.name = name;
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof RefConfigSection)) {
-      return false;
-    }
-    return name.equals(((RefConfigSection) obj).name);
-  }
-
-  @Override
-  public int hashCode() {
-    return name.hashCode();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SshHostKey.java b/java/com/google/gerrit/common/data/SshHostKey.java
deleted file mode 100644
index 05f1611..0000000
--- a/java/com/google/gerrit/common/data/SshHostKey.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-/** Description of the SSH daemon host key used by Gerrit. */
-public class SshHostKey {
-  protected String hostIdent;
-  protected String hostKey;
-  protected String fingerprint;
-
-  protected SshHostKey() {}
-
-  public SshHostKey(String hi, String hk, String fp) {
-    hostIdent = hi;
-    hostKey = hk;
-    fingerprint = fp;
-  }
-
-  /** @return host name string, to appear in a known_hosts file. */
-  public String getHostIdent() {
-    return hostIdent;
-  }
-
-  /** @return base 64 encoded host key string, starting with key type. */
-  public String getHostKey() {
-    return hostKey;
-  }
-
-  /** @return the key fingerprint, as displayed by a connecting client. */
-  public String getFingerprint() {
-    return fingerprint;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
index 8638d6d..22861b2 100644
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.reviewdb.client.Account;
 import java.util.Collection;
 import java.util.List;
@@ -65,7 +64,7 @@
 
   public Status status;
   public List<Label> labels;
-  @GwtIncompatible public List<SubmitRequirement> requirements;
+  public List<SubmitRequirement> requirements;
   public String errorMessage;
 
   public static class Label {
@@ -136,7 +135,6 @@
     }
   }
 
-  @GwtIncompatible
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
@@ -164,7 +162,6 @@
     return sb.toString();
   }
 
-  @GwtIncompatible
   @Override
   public boolean equals(Object o) {
     if (o instanceof SubmitRecord) {
@@ -177,7 +174,6 @@
     return false;
   }
 
-  @GwtIncompatible
   @Override
   public int hashCode() {
     return Objects.hash(status, labels, errorMessage, requirements);
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
index 0c978ca..66e647d 100644
--- a/java/com/google/gerrit/common/data/SubmitRequirement.java
+++ b/java/com/google/gerrit/common/data/SubmitRequirement.java
@@ -17,13 +17,11 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.annotations.GwtIncompatible;
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
 
 /** Describes a requirement to submit a change. */
-@GwtIncompatible
 @AutoValue
 @AutoValue.CopyAnnotations
 public abstract class SubmitRequirement {
diff --git a/java/com/google/gerrit/common/data/SubscribeSection.java b/java/com/google/gerrit/common/data/SubscribeSection.java
index a3468d7..aaf0798 100644
--- a/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.annotations.GwtIncompatible;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.ArrayList;
@@ -24,7 +23,6 @@
 import org.eclipse.jgit.transport.RefSpec;
 
 /** Portion of a {@link Project} describing superproject subscription rules. */
-@GwtIncompatible("Unemulated org.eclipse.jgit.transport.RefSpec")
 public class SubscribeSection {
 
   private final List<RefSpec> multiMatchRefSpecs;
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index 1988d66..265d590 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -20,14 +20,17 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
 
   public static GroupReferenceSubject assertThat(GroupReference group) {
-    return assertAbout(GroupReferenceSubject::new).that(group);
+    return assertAbout(groupReferences()).that(group);
+  }
+
+  public static Subject.Factory<GroupReferenceSubject, GroupReference> groupReferences() {
+    return GroupReferenceSubject::new;
   }
 
   private GroupReferenceSubject(FailureMetadata metadata, GroupReference group) {
@@ -37,12 +40,12 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getUUID());
   }
 
   public StringSubject name() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 }
diff --git a/java/com/google/gerrit/common/errors/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 ef4ef40..86f1083 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.elasticsearch.builders.QueryBuilder;
 import com.google.gerrit.elasticsearch.builders.SearchSourceBuilder;
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
@@ -53,7 +54,6 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import com.google.gwtorm.server.OrmException;
 import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.io.InputStream;
@@ -167,23 +167,23 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     IndexUtils.setReady(sitePaths, indexNameRaw, schema.getVersion(), ready);
   }
 
   @Override
-  public void delete(K id) throws IOException {
+  public void delete(K id) {
     String uri = getURI(type, BULK);
     Response response = postRequest(uri, getDeleteActions(id), getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format("Failed to delete %s from index %s: %s", id, indexName, statusCode));
     }
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     // Delete the index, if it exists.
     String endpoint = indexName + client.adapter().indicesExistParam();
     Response response = performRequest("HEAD", endpoint);
@@ -192,18 +192,20 @@
       response = performRequest("DELETE", indexName);
       statusCode = response.getStatusLine().getStatusCode();
       if (statusCode != HttpStatus.SC_OK) {
-        throw new IOException(
+        throw new StorageException(
             String.format("Failed to delete index %s: %s", indexName, statusCode));
       }
     }
 
     // Recreate the index.
     String indexCreationFields = concatJsonString(getSettings(), getMappings());
-    response = performRequest("PUT", indexName, indexCreationFields);
+    response =
+        performRequest(
+            "PUT", indexName + client.adapter().includeTypeNameParam(), indexCreationFields);
     statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       String error = String.format("Failed to create index %s: %s", indexName, statusCode);
-      throw new IOException(error);
+      throw new StorageException(error);
     }
   }
 
@@ -305,21 +307,24 @@
     return sortArray;
   }
 
-  protected String getURI(String type, String request) throws UnsupportedEncodingException {
-    String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
-    if (SEARCH.equals(request) && client.adapter().omitTypeFromSearch()) {
-      return encodedIndexName + "/" + request;
+  protected String getURI(String type, String request) {
+    try {
+      String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
+      if (SEARCH.equals(request) && client.adapter().omitTypeFromSearch()) {
+        return encodedIndexName + "/" + request;
+      }
+      String encodedType = URLEncoder.encode(type, UTF_8.toString());
+      return encodedIndexName + "/" + encodedType + "/" + request;
+    } catch (UnsupportedEncodingException e) {
+      throw new StorageException(e);
     }
-    String encodedType = URLEncoder.encode(type, UTF_8.toString());
-    return encodedIndexName + "/" + encodedType + "/" + request;
   }
 
-  protected Response postRequest(String uri, Object payload) throws IOException {
+  protected Response postRequest(String uri, Object payload) {
     return performRequest("POST", uri, payload);
   }
 
-  protected Response postRequest(String uri, Object payload, Map<String, String> params)
-      throws IOException {
+  protected Response postRequest(String uri, Object payload, Map<String, String> params) {
     return performRequest("POST", uri, payload, params);
   }
 
@@ -327,18 +332,16 @@
     return target.substring(0, target.length() - 1) + "," + addition.substring(1);
   }
 
-  private Response performRequest(String method, String uri) throws IOException {
+  private Response performRequest(String method, String uri) {
     return performRequest(method, uri, null);
   }
 
-  private Response performRequest(String method, String uri, @Nullable Object payload)
-      throws IOException {
+  private Response performRequest(String method, String uri, @Nullable Object payload) {
     return performRequest(method, uri, payload, Collections.emptyMap());
   }
 
   private Response performRequest(
-      String method, String uri, @Nullable Object payload, Map<String, String> params)
-      throws IOException {
+      String method, String uri, @Nullable Object payload, Map<String, String> params) {
     Request request = new Request(method, uri.startsWith("/") ? uri : "/" + uri);
     if (payload != null) {
       String payloadStr = payload instanceof String ? (String) payload : payload.toString();
@@ -347,7 +350,11 @@
     for (Map.Entry<String, String> entry : params.entrySet()) {
       request.addParameter(entry.getKey(), entry.getValue());
     }
-    return client.get().performRequest(request);
+    try {
+      return client.get().performRequest(request);
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
   }
 
   protected class ElasticQuerySource implements DataSource<V> {
@@ -375,16 +382,16 @@
     }
 
     @Override
-    public ResultSet<V> read() throws OrmException {
+    public ResultSet<V> read() {
       return readImpl((doc) -> AbstractElasticIndex.this.fromDocument(doc, opts.fields()));
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       return readImpl(AbstractElasticIndex.this::toFieldBundle);
     }
 
-    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
+    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) {
       try {
         String uri = getURI(index, SEARCH);
         Response response =
@@ -410,7 +417,7 @@
         }
         return new ListResultSet<>(ImmutableList.of());
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
   }
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index d5c586b..f919aad 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -4,6 +4,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
@@ -14,7 +15,6 @@
         "//java/com/google/gerrit/server",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/commons:codec",
         "//lib/commons:lang",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index 1b69b6d..c25aa90 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -38,7 +39,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -73,7 +73,7 @@
   }
 
   @Override
-  public void replace(AccountState as) throws IOException {
+  public void replace(AccountState as) {
     BulkRequest bulk =
         new IndexRequest(getId(as), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, as));
@@ -82,7 +82,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace account %s in index %s: %s",
               as.getAccount().getId(), indexName, statusCode));
@@ -93,8 +93,7 @@
   public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
       throws QueryParseException {
     JsonArray sortArray = getSortArray(AccountField.ID.getName());
-    return new ElasticQuerySource(
-        p, opts.filterFields(IndexUtils::accountFields), ACCOUNTS, sortArray);
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::accountFields), type, sortArray);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index cf4022b..5782399 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -40,7 +41,6 @@
 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.converter.ChangeProtoConverter;
 import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
@@ -58,10 +58,8 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -108,20 +106,16 @@
   }
 
   @Override
-  public void replace(ChangeData cd) throws IOException {
+  public void replace(ChangeData cd) {
     String deleteIndex;
     String insertIndex;
 
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        insertIndex = OPEN_CHANGES;
-        deleteIndex = CLOSED_CHANGES;
-      } else {
-        insertIndex = CLOSED_CHANGES;
-        deleteIndex = OPEN_CHANGES;
-      }
-    } catch (OrmException e) {
-      throw new IOException(e);
+    if (cd.change().isNew()) {
+      insertIndex = OPEN_CHANGES;
+      deleteIndex = CLOSED_CHANGES;
+    } else {
+      insertIndex = CLOSED_CHANGES;
+      deleteIndex = OPEN_CHANGES;
     }
 
     ElasticQueryAdapter adapter = client.adapter();
@@ -136,7 +130,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace change %s in index %s: %s", cd.getId(), indexName, statusCode));
     }
@@ -181,7 +175,7 @@
   }
 
   @Override
-  protected String getDeleteActions(Id c) {
+  protected String getDeleteActions(Change.Id c) {
     if (client.adapter().usePostV5Type()) {
       return delete(ElasticQueryAdapter.POST_V5_TYPE, c);
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index 6863238..5f48499 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -26,7 +26,6 @@
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 import org.apache.http.HttpHost;
 import org.eclipse.jgit.lib.Config;
 
@@ -37,24 +36,20 @@
   static final String SECTION_ELASTICSEARCH = "elasticsearch";
   static final String KEY_PASSWORD = "password";
   static final String KEY_USERNAME = "username";
-  static final String KEY_MAX_RETRY_TIMEOUT = "maxRetryTimeout";
   static final String KEY_PREFIX = "prefix";
   static final String KEY_SERVER = "server";
   static final String KEY_NUMBER_OF_SHARDS = "numberOfShards";
   static final String KEY_NUMBER_OF_REPLICAS = "numberOfReplicas";
   static final String DEFAULT_PORT = "9200";
   static final String DEFAULT_USERNAME = "elastic";
-  static final int DEFAULT_MAX_RETRY_TIMEOUT_MS = 30000;
   static final int DEFAULT_NUMBER_OF_SHARDS = 5;
   static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
-  static final TimeUnit MAX_RETRY_TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
 
   private final Config cfg;
   private final List<HttpHost> hosts;
 
   final String username;
   final String password;
-  final int maxRetryTimeout;
   final int numberOfShards;
   final int numberOfReplicas;
   final String prefix;
@@ -68,14 +63,6 @@
             ? null
             : firstNonNull(
                 cfg.getString(SECTION_ELASTICSEARCH, null, KEY_USERNAME), DEFAULT_USERNAME);
-    this.maxRetryTimeout =
-        (int)
-            cfg.getTimeUnit(
-                SECTION_ELASTICSEARCH,
-                null,
-                KEY_MAX_RETRY_TIMEOUT,
-                DEFAULT_MAX_RETRY_TIMEOUT_MS,
-                MAX_RETRY_TIMEOUT_UNIT);
     this.prefix = Strings.nullToEmpty(cfg.getString(SECTION_ELASTICSEARCH, null, KEY_PREFIX));
     this.numberOfShards =
         cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_NUMBER_OF_SHARDS, DEFAULT_NUMBER_OF_SHARDS);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index f694a05..ecda1ee 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(InternalGroup group) throws IOException {
+  public void replace(InternalGroup group) {
     BulkRequest bulk =
         new IndexRequest(getId(group), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, group));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace group %s in index %s: %s",
               group.getGroupUUID().get(), indexName, statusCode));
@@ -91,7 +91,7 @@
   public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
       throws QueryParseException {
     JsonArray sortArray = getSortArray(GroupField.UUID.getName());
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), GROUPS, sortArray);
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), type, sortArray);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
index a777f47..100022a 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
@@ -54,11 +54,8 @@
     }
 
     return new JsonParser()
-        .parse(AbstractElasticIndex.getContent(response))
-        .getAsJsonObject()
-        .entrySet()
-        .stream()
-        .map(e -> e.getKey().replace(name, ""))
-        .collect(toList());
+        .parse(AbstractElasticIndex.getContent(response)).getAsJsonObject().entrySet().stream()
+            .map(e -> e.getKey().replace(name, ""))
+            .collect(toList());
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 8510559..daf3702 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectData;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(ProjectData projectState) throws IOException {
+  public void replace(ProjectData projectState) {
     BulkRequest bulk =
         new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, projectState));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace project %s in index %s: %s",
               projectState.getProject().getName(), indexName, statusCode));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 40c1bbb..23b6ffd 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -30,6 +30,7 @@
   private final String indexProperty;
   private final String rawFieldsKey;
   private final String versionDiscoveryUrl;
+  private final String includeTypeNameParam;
 
   ElasticQueryAdapter(ElasticVersion version) {
     this.ignoreUnmapped = false;
@@ -42,6 +43,7 @@
     this.stringFieldType = "text";
     this.indexProperty = "true";
     this.rawFieldsKey = "_source";
+    this.includeTypeNameParam = version.isV7OrLater() ? "?include_type_name=true" : "";
   }
 
   void setIgnoreUnmapped(JsonObject properties) {
@@ -95,4 +97,8 @@
   String getVersionDiscoveryUrl(String name) {
     return String.format(versionDiscoveryUrl, name);
   }
+
+  String includeTypeNameParam() {
+    return includeTypeNameParam;
+  }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
index e9839b7..a67de44 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -128,7 +128,6 @@
 
   private RestClient build() {
     RestClientBuilder builder = RestClient.builder(cfg.getHosts());
-    builder.setMaxRetryTimeoutMillis(cfg.maxRetryTimeout);
     setConfiguredCredentialsIfAny(builder);
     return builder.build();
   }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index b69f8f9..6de4d97 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -23,6 +23,8 @@
   V6_3("6.3.*"),
   V6_4("6.4.*"),
   V6_5("6.5.*"),
+  V6_6("6.6.*"),
+  V6_7("6.7.*"),
   V7_0("7.0.*");
 
   private final String version;
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
new file mode 100644
index 0000000..50bf883
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -0,0 +1,6 @@
+java_library(
+    name = "exceptions",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = ["//java/com/google/gerrit/reviewdb:server"],
+)
diff --git a/java/com/google/gerrit/common/errors/EmailException.java b/java/com/google/gerrit/exceptions/DuplicateKeyException.java
similarity index 63%
copy from java/com/google/gerrit/common/errors/EmailException.java
copy to java/com/google/gerrit/exceptions/DuplicateKeyException.java
index 635335d..d052450 100644
--- a/java/com/google/gerrit/common/errors/EmailException.java
+++ b/java/com/google/gerrit/exceptions/DuplicateKeyException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright 2009 Google Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,18 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
-public class EmailException extends Exception {
+/** Indicates one or more entities were concurrently inserted with the same key. */
+public class DuplicateKeyException extends StorageException {
   private static final long serialVersionUID = 1L;
 
-  public static final String MESSAGE = "Mail Error: ";
-
-  public EmailException(String msg) {
-    super(MESSAGE + msg);
+  public DuplicateKeyException(String msg) {
+    super(msg);
   }
 
-  public EmailException(String msg, Throwable why) {
-    super(MESSAGE + msg, why);
+  public DuplicateKeyException(String msg, Throwable why) {
+    super(msg, why);
   }
 }
diff --git a/java/com/google/gerrit/common/errors/EmailException.java b/java/com/google/gerrit/exceptions/EmailException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/EmailException.java
rename to java/com/google/gerrit/exceptions/EmailException.java
index 635335d..a278eed 100644
--- a/java/com/google/gerrit/common/errors/EmailException.java
+++ b/java/com/google/gerrit/exceptions/EmailException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 public class EmailException extends Exception {
   private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/common/errors/InvalidNameException.java b/java/com/google/gerrit/exceptions/InvalidNameException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/InvalidNameException.java
rename to java/com/google/gerrit/exceptions/InvalidNameException.java
index d975aef..4539500 100644
--- a/java/com/google/gerrit/common/errors/InvalidNameException.java
+++ b/java/com/google/gerrit/exceptions/InvalidNameException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error indicating the entity name is invalid as supplied. */
 public class InvalidNameException extends Exception {
diff --git a/java/com/google/gerrit/common/errors/InvalidSshKeyException.java b/java/com/google/gerrit/exceptions/InvalidSshKeyException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/InvalidSshKeyException.java
rename to java/com/google/gerrit/exceptions/InvalidSshKeyException.java
index 3398417..8baba20 100644
--- a/java/com/google/gerrit/common/errors/InvalidSshKeyException.java
+++ b/java/com/google/gerrit/exceptions/InvalidSshKeyException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error indicating the SSH key string is invalid as supplied. */
 public class InvalidSshKeyException extends Exception {
diff --git a/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java b/java/com/google/gerrit/exceptions/NameAlreadyUsedException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
rename to java/com/google/gerrit/exceptions/NameAlreadyUsedException.java
index ea20e2e..df2631b 100644
--- a/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
+++ b/java/com/google/gerrit/exceptions/NameAlreadyUsedException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error indicating entity name is already taken by another entity. */
 public class NameAlreadyUsedException extends Exception {
diff --git a/java/com/google/gerrit/common/errors/NoSuchAccountException.java b/java/com/google/gerrit/exceptions/NoSuchAccountException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/NoSuchAccountException.java
rename to java/com/google/gerrit/exceptions/NoSuchAccountException.java
index 90bf624..d753128 100644
--- a/java/com/google/gerrit/common/errors/NoSuchAccountException.java
+++ b/java/com/google/gerrit/exceptions/NoSuchAccountException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error indicating the account requested doesn't exist. */
 public class NoSuchAccountException extends Exception {
diff --git a/java/com/google/gerrit/common/errors/NoSuchEntityException.java b/java/com/google/gerrit/exceptions/NoSuchEntityException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/NoSuchEntityException.java
rename to java/com/google/gerrit/exceptions/NoSuchEntityException.java
index 1829c8b..c812a38 100644
--- a/java/com/google/gerrit/common/errors/NoSuchEntityException.java
+++ b/java/com/google/gerrit/exceptions/NoSuchEntityException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error indicating the entity requested doesn't exist. */
 public class NoSuchEntityException extends Exception {
diff --git a/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
similarity index 97%
rename from java/com/google/gerrit/common/errors/NoSuchGroupException.java
rename to java/com/google/gerrit/exceptions/NoSuchGroupException.java
index 6e3db9e..dca28cb 100644
--- a/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ b/java/com/google/gerrit/exceptions/NoSuchGroupException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
diff --git a/java/com/google/gerrit/common/errors/NotSignedInException.java b/java/com/google/gerrit/exceptions/NotSignedInException.java
similarity index 95%
rename from java/com/google/gerrit/common/errors/NotSignedInException.java
rename to java/com/google/gerrit/exceptions/NotSignedInException.java
index 65caf02..1919dc39 100644
--- a/java/com/google/gerrit/common/errors/NotSignedInException.java
+++ b/java/com/google/gerrit/exceptions/NotSignedInException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.exceptions;
 
 /** Error stating the user must be signed-in in order to perform this action. */
 public class NotSignedInException extends Exception {
diff --git a/java/com/google/gerrit/exceptions/StorageException.java b/java/com/google/gerrit/exceptions/StorageException.java
new file mode 100644
index 0000000..a788fff
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/StorageException.java
@@ -0,0 +1,44 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.exceptions;
+
+/**
+ * Any read/write error in a storage layer.
+ *
+ * <p>This includes but is not limited to:
+ *
+ * <ul>
+ *   <li>NoteDb exceptions
+ *   <li>Secondary index exceptions
+ *   <li>{@code AccountPatchReviewStore} exceptions
+ *   <li>Wrapped JGit exceptions
+ *   <li>Other wrapped {@code IOException}s
+ * </ul>
+ */
+public class StorageException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public StorageException(String message) {
+    super(message);
+  }
+
+  public StorageException(String message, Throwable why) {
+    super(message, why);
+  }
+
+  public StorageException(Throwable why) {
+    super(why);
+  }
+}
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index 5f9220e..b69d2c8 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -1,5 +1,6 @@
-load("//lib:guava.bzl", "GUAVA_DOC_URL")
 load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL")
+load("//lib:guava.bzl", "GUAVA_DOC_URL")
+load("//tools/bzl:javadoc.bzl", "java_doc")
 
 java_binary(
     name = "extension-api",
@@ -29,13 +30,12 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib/auto:auto-value-annotations",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
     ],
 )
 
-load("//tools/bzl:javadoc.bzl", "java_doc")
-
 java_doc(
     name = "extension-api-javadoc",
     external_docs = [
diff --git a/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/java/com/google/gerrit/extensions/annotations/ExportImpl.java
deleted file mode 100644
index a3e72bc..0000000
--- a/java/com/google/gerrit/extensions/annotations/ExportImpl.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.annotations;
-
-import java.io.Serializable;
-import java.lang.annotation.Annotation;
-
-final class ExportImpl implements Export, Serializable {
-  private static final long serialVersionUID = 0;
-  private final String value;
-
-  ExportImpl(String value) {
-    this.value = value;
-  }
-
-  @Override
-  public Class<? extends Annotation> annotationType() {
-    return Export.class;
-  }
-
-  @Override
-  public String value() {
-    return value;
-  }
-
-  @Override
-  public int hashCode() {
-    return (127 * "value".hashCode()) ^ value.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return o instanceof Export && value.equals(((Export) o).value());
-  }
-
-  @Override
-  public String toString() {
-    return "@" + Export.class.getName() + "(value=" + value + ")";
-  }
-}
diff --git a/java/com/google/gerrit/extensions/annotations/Exports.java b/java/com/google/gerrit/extensions/annotations/Exports.java
index 1295ea0..9b196b6 100644
--- a/java/com/google/gerrit/extensions/annotations/Exports.java
+++ b/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.extensions.annotations;
 
+import com.google.auto.value.AutoAnnotation;
+
 /** Static constructors for {@link Export} annotations. */
 public final class Exports {
   /** Create an annotation to export under a specific name. */
-  public static Export named(String name) {
-    return new ExportImpl(name);
+  @AutoAnnotation
+  public static Export named(String value) {
+    return new AutoAnnotation_Exports_named(value);
   }
 
   /** Create an annotation to export based on a cannonical class name. */
diff --git a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java
similarity index 65%
copy from java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
copy to java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java
index a795025..de68987 100644
--- a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
+++ b/java/com/google/gerrit/extensions/api/access/CoreOrPluginProjectPermission.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.query.change;
+package com.google.gerrit.extensions.api.access;
 
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import java.util.List;
-
-public interface PluginDefinedAttributesFactory {
-  List<PluginDefinedInfo> create(ChangeData cd);
-}
+/** 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/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 651e786..db7f506 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -172,16 +172,19 @@
       return this;
     }
 
+    /** Set an option on the request, appending to existing options. */
     public QueryRequest withOption(ListAccountsOption options) {
       this.options.add(options);
       return this;
     }
 
+    /** Set options on the request, appending to existing options. */
     public QueryRequest withOptions(ListAccountsOption... options) {
       this.options.addAll(Arrays.asList(options));
       return this;
     }
 
+    /** Set options on the request, replacing existing options. */
     public QueryRequest withOptions(EnumSet<ListAccountsOption> options) {
       this.options = options;
       return this;
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 14e0ed7..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;
@@ -215,7 +216,23 @@
     return suggestReviewers().withQuery(query);
   }
 
-  ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
+  /**
+   * Retrieve reviewers ({@code ReviewerState.REVIEWER} and {@code ReviewerState.CC}) on the change.
+   */
+  List<ReviewerInfo> reviewers() 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));
@@ -473,7 +490,19 @@
     }
 
     @Override
-    public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
+    public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ReviewerInfo> reviewers() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo get(
+        EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
index 9d0275a..25eb7a8 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.client.ChangeEditDetailOption;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.EnumSet;
 import java.util.Optional;
 
 /**
@@ -29,6 +31,33 @@
  */
 public interface ChangeEditApi {
 
+  abstract class ChangeEditDetailRequest {
+    private String base;
+    private EnumSet<ChangeEditDetailOption> options = EnumSet.noneOf(ChangeEditDetailOption.class);
+
+    public abstract Optional<EditInfo> get() throws RestApiException;
+
+    public ChangeEditDetailRequest withBase(String base) {
+      this.base = base;
+      return this;
+    }
+
+    public ChangeEditDetailRequest withOption(ChangeEditDetailOption option) {
+      this.options.add(option);
+      return this;
+    }
+
+    public String getBase() {
+      return base;
+    }
+
+    public EnumSet<ChangeEditDetailOption> options() {
+      return options;
+    }
+  }
+
+  ChangeEditDetailRequest detail() throws RestApiException;
+
   /**
    * Retrieves details regarding the change edit.
    *
@@ -156,6 +185,11 @@
    */
   class NotImplemented implements ChangeEditApi {
     @Override
+    public ChangeEditDetailRequest detail() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Optional<EditInfo> get() 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 0708ef5..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,26 +93,46 @@
       return this;
     }
 
+    public QueryRequest withNoLimit() {
+      this.isNoLimit = true;
+      return this;
+    }
+
     public QueryRequest withStart(int start) {
       this.start = start;
       return this;
     }
 
+    /** Set an option on the request, appending to existing options. */
     public QueryRequest withOption(ListChangesOption options) {
       this.options.add(options);
       return this;
     }
 
+    /** Set options on the request, appending to existing options. */
     public QueryRequest withOptions(ListChangesOption... options) {
       this.options.addAll(Arrays.asList(options));
       return this;
     }
 
+    /** Set options on the request, replacing existing options. */
     public QueryRequest withOptions(EnumSet<ListChangesOption> options) {
       this.options = options;
       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;
     }
@@ -117,6 +141,10 @@
       return limit;
     }
 
+    public boolean getNoLimit() {
+      return isNoLimit;
+    }
+
     public int getStart() {
       return start;
     }
@@ -125,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);
@@ -137,7 +169,11 @@
       if (!options.isEmpty()) {
         sb.append("options=").append(options);
       }
-      return sb.append('}').toString();
+      sb.append('}');
+      if (isNoLimit == true) {
+        sb.append(" --no-limit");
+      }
+      return sb.toString();
     }
   }
 
diff --git a/java/com/google/gerrit/extensions/api/changes/FileApi.java b/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 89dc269..39cf2b7 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -39,6 +39,9 @@
    */
   DiffRequest diffRequest() throws RestApiException;
 
+  /** Set the file reviewed or not reviewed */
+  void setReviewed(boolean reviewed) throws RestApiException;
+
   abstract class DiffRequest {
     private String base;
     private Integer context;
@@ -123,5 +126,10 @@
     public DiffRequest diffRequest() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void setReviewed(boolean reviewed) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index a6df45f..7d356bf 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,9 +14,11 @@
 
 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;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -150,6 +152,9 @@
 
   RelatedChangesInfo related() throws RestApiException;
 
+  /** Returns votes on the revision. */
+  ListMultimap<String, ApprovalInfo> votes() throws RestApiException;
+
   abstract class MergeListRequest {
     private boolean addLinks;
     private int uninterestingParent = 1;
@@ -361,6 +366,11 @@
     }
 
     @Override
+    public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void description(String description) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/groups/Groups.java b/java/com/google/gerrit/extensions/api/groups/Groups.java
index 0243ba3..86c2d77 100644
--- a/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -253,16 +253,19 @@
       return this;
     }
 
+    /** Set an option on the request, appending to existing options. */
     public QueryRequest withOption(ListGroupsOption options) {
       this.options.add(options);
       return this;
     }
 
+    /** Set options on the request, appending to existing options. */
     public QueryRequest withOptions(ListGroupsOption... options) {
       this.options.addAll(Arrays.asList(options));
       return this;
     }
 
+    /** Set options on the request, replacing existing options. */
     public QueryRequest withOptions(EnumSet<ListGroupsOption> options) {
       this.options = options;
       return this;
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/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 0139b52..3d70996 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -106,6 +106,8 @@
 
   List<ProjectInfo> children(boolean recursive) throws RestApiException;
 
+  List<ProjectInfo> children(int limit) throws RestApiException;
+
   ChildProjectApi child(String name) throws RestApiException;
 
   /**
@@ -285,6 +287,11 @@
     }
 
     @Override
+    public List<ProjectInfo> children(int limit) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChildProjectApi child(String name) throws RestApiException {
       throw new NotImplementedException();
     }
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/server/query/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/extensions/client/ChangeEditDetailOption.java
similarity index 65%
copy from java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
copy to java/com/google/gerrit/extensions/client/ChangeEditDetailOption.java
index a795025..156b768 100644
--- a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
+++ b/java/com/google/gerrit/extensions/client/ChangeEditDetailOption.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.query.change;
+package com.google.gerrit.extensions.client;
 
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import java.util.List;
-
-public interface PluginDefinedAttributesFactory {
-  List<PluginDefinedInfo> create(ChangeData cd);
+public enum ChangeEditDetailOption {
+  LIST_FILES,
+  DOWNLOAD_COMMANDS
 }
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 ffc5029..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 change details. */
-public enum ListChangesOption {
+/** Output options available for retrieval of change details. */
+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/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 703235d..e40004b 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
 
 public class ApprovalInfo extends AccountInfo {
@@ -28,7 +29,11 @@
   }
 
   public ApprovalInfo(
-      Integer id, Integer value, VotingRangeInfo permittedVotingRange, String tag, Timestamp date) {
+      Integer id,
+      Integer value,
+      @Nullable VotingRangeInfo permittedVotingRange,
+      @Nullable String tag,
+      Timestamp date) {
     super(id);
     this.value = value;
     this.permittedVotingRange = permittedVotingRange;
diff --git a/java/com/google/gerrit/extensions/common/GerritInfo.java b/java/com/google/gerrit/extensions/common/GerritInfo.java
index e825f2e..4746273 100644
--- a/java/com/google/gerrit/extensions/common/GerritInfo.java
+++ b/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -22,4 +22,5 @@
   public Boolean editGpgKeys;
   public String reportBugUrl;
   public String reportBugText;
+  public String primaryWeblinkName;
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 6679104..7092d21 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -6,6 +6,7 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/truth",
+        "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
diff --git a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
index 6dd5ce4..f0f5516 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -15,18 +15,23 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.GitPersonSubject.gitPersons;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.truth.ListSubject;
 
 public class CommitInfoSubject extends Subject<CommitInfoSubject, CommitInfo> {
 
   public static CommitInfoSubject assertThat(CommitInfo commitInfo) {
-    return assertAbout(CommitInfoSubject::new).that(commitInfo);
+    return assertAbout(commits()).that(commitInfo);
+  }
+
+  public static Subject.Factory<CommitInfoSubject, CommitInfo> commits() {
+    return CommitInfoSubject::new;
   }
 
   private CommitInfoSubject(FailureMetadata failureMetadata, CommitInfo commitInfo) {
@@ -36,31 +41,30 @@
   public StringSubject commit() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.commit).named("commit");
+    return check("commit()").that(commitInfo.commit);
   }
 
   public ListSubject<CommitInfoSubject, CommitInfo> parents() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return ListSubject.assertThat(commitInfo.parents, CommitInfoSubject::assertThat)
-        .named("parents");
+    return check("parents()").about(elements()).thatCustom(commitInfo.parents, commits());
   }
 
   public GitPersonSubject committer() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.committer).named("committer");
+    return check("committer()").about(gitPersons()).that(commitInfo.committer);
   }
 
   public GitPersonSubject author() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.author).named("author");
+    return check("author()").about(gitPersons()).that(commitInfo.author);
   }
 
   public StringSubject message() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.message).named("message");
+    return check("message").that(commitInfo.message);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
index 5fc8ba6..25750c1 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -14,21 +14,27 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
 
 public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> {
 
   public static ContentEntrySubject assertThat(ContentEntry contentEntry) {
-    return assertAbout(ContentEntrySubject::new).that(contentEntry);
+    return assertAbout(contentEntries()).that(contentEntry);
+  }
+
+  public static Subject.Factory<ContentEntrySubject, ContentEntry> contentEntries() {
+    return ContentEntrySubject::new;
   }
 
   private ContentEntrySubject(FailureMetadata failureMetadata, ContentEntry contentEntry) {
@@ -38,54 +44,54 @@
   public void isDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isTrue();
+    if (contentEntry.dueToRebase == null || !contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry to be marked 'dueToRebase'"));
+    }
   }
 
   public void isNotDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should not be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isNull();
+    if (contentEntry.dueToRebase != null && contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry not to be marked 'dueToRebase'"));
+    }
   }
 
   public ListSubject<StringSubject, String> commonLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.ab, Truth::assertThat).named("common lines");
+    return check("commonLines()")
+        .about(elements())
+        .that(contentEntry.ab, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.a, Truth::assertThat).named("lines of 'a'");
+    return check("linesOfA()").about(elements()).that(contentEntry.a, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'");
+    return check("linesOfB()").about(elements()).that(contentEntry.b, StandardSubjectBuilder::that);
   }
 
   public IterableSubject intralineEditsOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editA).named("intraline edits of 'a'");
+    return check("intralineEditsOfA()").that(contentEntry.editA);
   }
 
   public IterableSubject intralineEditsOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
+    return check("intralineEditsOfB()").that(contentEntry.editB);
   }
 
   public IntegerSubject numberOfSkippedLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.skip).named("number of skipped lines");
+    return check("numberOfSkippedLines()").that(contentEntry.skip);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index 057a1a2..ee37bde 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FileMetaSubject.fileMetas;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -38,25 +39,26 @@
   public ListSubject<ContentEntrySubject, ContentEntry> content() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return ListSubject.assertThat(diffInfo.content, ContentEntrySubject::assertThat)
-        .named("content");
+    return check("content()")
+        .about(elements())
+        .thatCustom(diffInfo.content, ContentEntrySubject.contentEntries());
   }
 
   public ComparableSubject<?, ChangeType> changeType() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return Truth.assertThat(diffInfo.changeType).named("changeType");
+    return check("changeType()").that(diffInfo.changeType);
   }
 
   public FileMetaSubject metaA() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+    return check("metaA()").about(fileMetas()).that(diffInfo.metaA);
   }
 
   public FileMetaSubject metaB() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaB).named("metaB");
+    return check("metaB()").about(fileMetas()).that(diffInfo.metaB);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
index 84ad61c..1c99141 100644
--- a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.truth.OptionalSubject;
 import java.util.Optional;
@@ -27,12 +27,16 @@
 public class EditInfoSubject extends Subject<EditInfoSubject, EditInfo> {
 
   public static EditInfoSubject assertThat(EditInfo editInfo) {
-    return assertAbout(EditInfoSubject::new).that(editInfo);
+    return assertAbout(edits()).that(editInfo);
+  }
+
+  private static Subject.Factory<EditInfoSubject, EditInfo> edits() {
+    return EditInfoSubject::new;
   }
 
   public static OptionalSubject<EditInfoSubject, EditInfo> assertThat(
       Optional<EditInfo> editInfoOptional) {
-    return OptionalSubject.assertThat(editInfoOptional, EditInfoSubject::assertThat);
+    return OptionalSubject.assertThat(editInfoOptional, edits());
   }
 
   private EditInfoSubject(FailureMetadata failureMetadata, EditInfo editInfo) {
@@ -42,12 +46,12 @@
   public CommitInfoSubject commit() {
     isNotNull();
     EditInfo editInfo = actual();
-    return CommitInfoSubject.assertThat(editInfo.commit).named("commit");
+    return check("commit()").about(commits()).that(editInfo.commit);
   }
 
   public StringSubject baseRevision() {
     isNotNull();
     EditInfo editInfo = actual();
-    return Truth.assertThat(editInfo.baseRevision).named("baseRevision");
+    return check("baseRevision()").that(editInfo.baseRevision);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
index b088016..3ebf838 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FileInfo;
 
 public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
@@ -36,18 +35,18 @@
   public IntegerSubject linesInserted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesInserted).named("linesInserted");
+    return check("linesInserted()").that(fileInfo.linesInserted);
   }
 
   public IntegerSubject linesDeleted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted");
+    return check("linesDeleted()").that(fileInfo.linesDeleted);
   }
 
   public ComparableSubject<?, Character> status() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.status).named("status");
+    return check("status()").that(fileInfo.status);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
index e77eef1..d1b2031 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -19,13 +19,16 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
 
 public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
 
   public static FileMetaSubject assertThat(FileMeta fileMeta) {
-    return assertAbout(FileMetaSubject::new).that(fileMeta);
+    return assertAbout(fileMetas()).that(fileMeta);
+  }
+
+  public static Subject.Factory<FileMetaSubject, FileMeta> fileMetas() {
+    return FileMetaSubject::new;
   }
 
   private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
@@ -35,6 +38,6 @@
   public IntegerSubject totalLineCount() {
     isNotNull();
     FileMeta fileMeta = actual();
-    return Truth.assertThat(fileMeta.lines).named("total line count");
+    return check("totalLineCount()").that(fileMeta.lines);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
index b56d399..6ba5f8b 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
@@ -15,18 +15,22 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 
 public class FixReplacementInfoSubject
     extends Subject<FixReplacementInfoSubject, FixReplacementInfo> {
 
   public static FixReplacementInfoSubject assertThat(FixReplacementInfo fixReplacementInfo) {
-    return assertAbout(FixReplacementInfoSubject::new).that(fixReplacementInfo);
+    return assertAbout(fixReplacements()).that(fixReplacementInfo);
+  }
+
+  public static Subject.Factory<FixReplacementInfoSubject, FixReplacementInfo> fixReplacements() {
+    return FixReplacementInfoSubject::new;
   }
 
   private FixReplacementInfoSubject(
@@ -35,14 +39,17 @@
   }
 
   public StringSubject path() {
-    return Truth.assertThat(actual().path).named("path");
+    isNotNull();
+    return check("path()").that(actual().path);
   }
 
   public RangeSubject range() {
-    return RangeSubject.assertThat(actual().range).named("range");
+    isNotNull();
+    return check("range()").about(ranges()).that(actual().range);
   }
 
   public StringSubject replacement() {
-    return Truth.assertThat(actual().replacement).named("replacement");
+    isNotNull();
+    return check("replacement()").that(actual().replacement);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
index 7a6da9c..98dac38 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FixReplacementInfoSubject.fixReplacements;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
@@ -27,7 +29,11 @@
 public class FixSuggestionInfoSubject extends Subject<FixSuggestionInfoSubject, FixSuggestionInfo> {
 
   public static FixSuggestionInfoSubject assertThat(FixSuggestionInfo fixSuggestionInfo) {
-    return assertAbout(FixSuggestionInfoSubject::new).that(fixSuggestionInfo);
+    return assertAbout(fixSuggestions()).that(fixSuggestionInfo);
+  }
+
+  public static Subject.Factory<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
+    return FixSuggestionInfoSubject::new;
   }
 
   private FixSuggestionInfoSubject(
@@ -40,8 +46,10 @@
   }
 
   public ListSubject<FixReplacementInfoSubject, FixReplacementInfo> replacements() {
-    return ListSubject.assertThat(actual().replacements, FixReplacementInfoSubject::assertThat)
-        .named("replacements");
+    isNotNull();
+    return check("replacements()")
+        .about(elements())
+        .thatCustom(actual().replacements, fixReplacements());
   }
 
   public FixReplacementInfoSubject onlyReplacement() {
@@ -49,6 +57,7 @@
   }
 
   public StringSubject description() {
-    return Truth.assertThat(actual().description).named("description");
+    isNotNull();
+    return check("description()").that(actual().description);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index cdbef34..dee0636 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.ComparableSubject;
@@ -21,7 +22,6 @@
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.GitPerson;
 import java.sql.Timestamp;
 import java.util.Date;
@@ -30,7 +30,11 @@
 public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
 
   public static GitPersonSubject assertThat(GitPerson gitPerson) {
-    return assertAbout(GitPersonSubject::new).that(gitPerson);
+    return assertAbout(gitPersons()).that(gitPerson);
+  }
+
+  public static Factory<GitPersonSubject, GitPerson> gitPersons() {
+    return GitPersonSubject::new;
   }
 
   private GitPersonSubject(FailureMetadata failureMetadata, GitPerson gitPerson) {
@@ -40,30 +44,30 @@
   public StringSubject name() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.name).named("name");
+    return check("name()").that(gitPerson.name);
   }
 
   public StringSubject email() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.email).named("email");
+    return check("email()").that(gitPerson.email);
   }
 
   public ComparableSubject<?, Timestamp> date() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.date).named("date");
+    return check("date()").that(gitPerson.date);
   }
 
   public IntegerSubject tz() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.tz).named("tz");
+    return check("tz()").that(gitPerson.tz);
   }
 
   public void hasSameDateAs(GitPerson other) {
+    checkNotNull(other, "'other' GitPerson must not be null");
     isNotNull();
-    assertThat(other).named("other").isNotNull();
     date().isEqualTo(other.date);
     tz().isEqualTo(other.tz);
   }
@@ -72,9 +76,7 @@
     isNotNull();
     name().isEqualTo(ident.getName());
     email().isEqualTo(ident.getEmailAddress());
-    Truth.assertThat(new Date(actual().date.getTime()))
-        .named("rounded date")
-        .isEqualTo(ident.getWhen());
+    check("roundedDate()").that(new Date(actual().date.getTime())).isEqualTo(ident.getWhen());
     tz().isEqualTo(ident.getTimeZoneOffset());
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
index db7f0d1..12acb8d 100644
--- a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
@@ -14,19 +14,22 @@
 
 package com.google.gerrit.extensions.common.testing;
 
-import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.client.Comment;
 
 public class RangeSubject extends Subject<RangeSubject, Comment.Range> {
 
   public static RangeSubject assertThat(Comment.Range range) {
-    return assertAbout(RangeSubject::new).that(range);
+    return assertAbout(ranges()).that(range);
+  }
+
+  public static Subject.Factory<RangeSubject, Comment.Range> ranges() {
+    return RangeSubject::new;
   }
 
   private RangeSubject(FailureMetadata failureMetadata, Comment.Range range) {
@@ -34,32 +37,32 @@
   }
 
   public IntegerSubject startLine() {
-    return Truth.assertThat(actual().startLine).named("startLine");
+    return check("startLine()").that(actual().startLine);
   }
 
   public IntegerSubject startCharacter() {
-    return Truth.assertThat(actual().startCharacter).named("startCharacter");
+    return check("startCharacter()").that(actual().startCharacter);
   }
 
   public IntegerSubject endLine() {
-    return Truth.assertThat(actual().endLine).named("endLine");
+    return check("endLine()").that(actual().endLine);
   }
 
   public IntegerSubject endCharacter() {
-    return Truth.assertThat(actual().endCharacter).named("endCharacter");
+    return check("endCharacter()").that(actual().endCharacter);
   }
 
   public void isValid() {
     isNotNull();
     if (!actual().isValid()) {
-      failWithoutActual(fact("expected", "valid"));
+      failWithActual(simpleFact("expected to be valid"));
     }
   }
 
   public void isInvalid() {
     isNotNull();
     if (actual().isValid()) {
-      failWithoutActual(fact("expected", "not valid"));
+      failWithActual(simpleFact("expected to be invalid"));
     }
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
index c2bed86..033f54b 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
@@ -27,12 +28,15 @@
 
   public static ListSubject<RobotCommentInfoSubject, RobotCommentInfo> assertThatList(
       List<RobotCommentInfo> robotCommentInfos) {
-    return ListSubject.assertThat(robotCommentInfos, RobotCommentInfoSubject::assertThat)
-        .named("robotCommentInfos");
+    return ListSubject.assertThat(robotCommentInfos, robotComments()).named("robotCommentInfos");
   }
 
   public static RobotCommentInfoSubject assertThat(RobotCommentInfo robotCommentInfo) {
-    return assertAbout(RobotCommentInfoSubject::new).that(robotCommentInfo);
+    return assertAbout(robotComments()).that(robotCommentInfo);
+  }
+
+  private static Factory<RobotCommentInfoSubject, RobotCommentInfo> robotComments() {
+    return RobotCommentInfoSubject::new;
   }
 
   private RobotCommentInfoSubject(
@@ -41,8 +45,9 @@
   }
 
   public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
-    return ListSubject.assertThat(actual().fixSuggestions, FixSuggestionInfoSubject::assertThat)
-        .named("fixSuggestions");
+    return check("fixSuggestions()")
+        .about(elements())
+        .thatCustom(actual().fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
   public FixSuggestionInfoSubject onlyFixSuggestion() {
diff --git a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
index aafb583..c76ec6c 100644
--- a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
+++ b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -18,7 +18,4 @@
 
 /** Specifies a capability declared by a plugin. */
 @ExtensionPoint
-public abstract class CapabilityDefinition {
-  /** @return description of the capability. */
-  public abstract String getDescription();
-}
+public abstract class CapabilityDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/common/FormatUtil.java b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
similarity index 61%
copy from java/com/google/gerrit/common/FormatUtil.java
copy to java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
index 0f6b37a..11b1981 100644
--- a/java/com/google/gerrit/common/FormatUtil.java
+++ b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gerrit.extensions.config;
 
-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);
-  }
+/** 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/common/FormatUtil.java b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
similarity index 61%
rename from java/com/google/gerrit/common/FormatUtil.java
rename to java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
index 0f6b37a..d1d9f9e 100644
--- a/java/com/google/gerrit/common/FormatUtil.java
+++ b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,14 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gerrit.extensions.config;
 
-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);
-  }
-}
+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/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 4d803b6..3f64ddb 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -222,8 +222,7 @@
    * @return sorted set of active plugins that supply at least one item.
    */
   public ImmutableSortedSet<String> plugins() {
-    return items
-        .stream()
+    return items.stream()
         .map(i -> i.get().getPluginName())
         .collect(toImmutableSortedSet(naturalOrder()));
   }
@@ -235,8 +234,7 @@
    * @return items exported by a plugin.
    */
   public ImmutableSet<Provider<T>> byPlugin(String pluginName) {
-    return items
-        .stream()
+    return items.stream()
         .filter(i -> i.get().getPluginName().equals(pluginName))
         .map(i -> i.get().getProvider())
         .collect(toImmutableSet());
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/restapi/RestApiException.java b/java/com/google/gerrit/extensions/restapi/RestApiException.java
index 28398a4..b09723e 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.restapi;
 
-/** Root exception type for JSON API failures. */
+/** Root exception type for REST API failures. */
 public class RestApiException extends Exception {
   private static final long serialVersionUID = 1L;
   private CacheControl caching = CacheControl.NONE;
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
index 1867308..5109205 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
+++ b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.PrimitiveByteArraySubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.ByteArrayOutputStream;
@@ -30,12 +29,16 @@
 public class BinaryResultSubject extends Subject<BinaryResultSubject, BinaryResult> {
 
   public static BinaryResultSubject assertThat(BinaryResult binaryResult) {
-    return assertAbout(BinaryResultSubject::new).that(binaryResult);
+    return assertAbout(binaryResults()).that(binaryResult);
+  }
+
+  private static Subject.Factory<BinaryResultSubject, BinaryResult> binaryResults() {
+    return BinaryResultSubject::new;
   }
 
   public static OptionalSubject<BinaryResultSubject, BinaryResult> assertThat(
       Optional<BinaryResult> binaryResultOptional) {
-    return OptionalSubject.assertThat(binaryResultOptional, BinaryResultSubject::assertThat);
+    return OptionalSubject.assertThat(binaryResultOptional, binaryResults());
   }
 
   private BinaryResultSubject(FailureMetadata failureMetadata, BinaryResult binaryResult) {
@@ -48,7 +51,7 @@
     // be used afterwards. Besides, closing it doesn't have an effect for most
     // implementations of a BinaryResult.
     BinaryResult binaryResult = actual();
-    return Truth.assertThat(binaryResult.asString());
+    return check("asString()").that(binaryResult.asString());
   }
 
   public PrimitiveByteArraySubject bytes() throws IOException {
@@ -60,6 +63,6 @@
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     binaryResult.writeTo(byteArrayOutputStream);
     byte[] bytes = byteArrayOutputStream.toByteArray();
-    return Truth.assertThat(bytes);
+    return check("bytes()").that(bytes);
   }
 }
diff --git a/java/com/google/gerrit/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
index b249749..9e67d70 100644
--- a/java/com/google/gerrit/git/LockFailureException.java
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -36,9 +36,7 @@
   public LockFailureException(String message, BatchRefUpdate batchRefUpdate) {
     super(message);
     refs =
-        batchRefUpdate
-            .getCommands()
-            .stream()
+        batchRefUpdate.getCommands().stream()
             .filter(c -> c.getResult() == ReceiveCommand.Result.LOCK_FAILURE)
             .map(ReceiveCommand::getRefName)
             .collect(toImmutableList());
diff --git a/java/com/google/gerrit/git/testing/CommitSubject.java b/java/com/google/gerrit/git/testing/CommitSubject.java
new file mode 100644
index 0000000..0873107
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/CommitSubject.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.git.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/** Subject over JGit {@link RevCommit}s. */
+public class CommitSubject extends Subject<CommitSubject, RevCommit> {
+
+  /**
+   * Constructs a new subject.
+   *
+   * @param commit the commit.
+   * @return a new subject over the commit.
+   */
+  public static CommitSubject assertThat(RevCommit commit) {
+    return assertAbout(CommitSubject::new).that(commit);
+  }
+
+  /**
+   * Performs some common assertions over a single commit.
+   *
+   * @param commit the commit.
+   * @param expectedCommitMessage exact expected commit message.
+   * @param expectedCommitTimestamp expected commit timestamp, to the tolerance specified in {@link
+   *     #hasCommitTimestamp(Timestamp)}.
+   * @param expectedSha1 expected commit SHA-1.
+   */
+  public static void assertCommit(
+      RevCommit commit,
+      String expectedCommitMessage,
+      Timestamp expectedCommitTimestamp,
+      ObjectId expectedSha1) {
+    CommitSubject commitSubject = assertThat(commit);
+    commitSubject.hasCommitMessage(expectedCommitMessage);
+    commitSubject.hasCommitTimestamp(expectedCommitTimestamp);
+    commitSubject.hasSha1(expectedSha1);
+  }
+
+  private CommitSubject(FailureMetadata metadata, RevCommit actual) {
+    super(metadata, actual);
+  }
+
+  /**
+   * Asserts that the commit has the given commit message.
+   *
+   * @param expectedCommitMessage exact expected commit message.
+   */
+  public void hasCommitMessage(String expectedCommitMessage) {
+    isNotNull();
+    RevCommit commit = actual();
+    check("commitMessage()").that(commit.getFullMessage()).isEqualTo(expectedCommitMessage);
+  }
+
+  /**
+   * Asserts that the commit has the given commit message, up to skew of at most 1 second.
+   *
+   * @param expectedCommitTimestamp expected commit timestamp.
+   */
+  public void hasCommitTimestamp(Timestamp expectedCommitTimestamp) {
+    isNotNull();
+    RevCommit commit = actual();
+    long timestampDiffMs =
+        Math.abs(commit.getCommitTime() * 1000L - expectedCommitTimestamp.getTime());
+    check("commitTimestampDiff()").that(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
+  }
+
+  /**
+   * Asserts that the commit has the given SHA-1.
+   *
+   * @param expectedSha1 expected commit SHA-1.
+   */
+  public void hasSha1(ObjectId expectedSha1) {
+    isNotNull();
+    RevCommit commit = actual();
+    check("sha1()").that(commit).isEqualTo(expectedSha1);
+  }
+}
diff --git a/java/com/google/gerrit/git/testing/ObjectIdSubject.java b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
new file mode 100644
index 0000000..5fe91f9
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.git.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ObjectIdSubject extends Subject<ObjectIdSubject, ObjectId> {
+  public static ObjectIdSubject assertThat(ObjectId objectId) {
+    return assertAbout(objectIds()).that(objectId);
+  }
+
+  public static Factory<ObjectIdSubject, ObjectId> objectIds() {
+    return ObjectIdSubject::new;
+  }
+
+  private ObjectIdSubject(FailureMetadata metadata, ObjectId actual) {
+    super(metadata, actual);
+  }
+
+  public void hasName(String expectedName) {
+    isNotNull();
+    ObjectId objectId = actual();
+    check("name()").that(objectId.getName()).isEqualTo(expectedName);
+  }
+}
diff --git a/java/com/google/gerrit/git/testing/PushResultSubject.java b/java/com/google/gerrit/git/testing/PushResultSubject.java
index 929e182..f5c9810 100644
--- a/java/com/google/gerrit/git/testing/PushResultSubject.java
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertAbout;
-import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
@@ -25,11 +24,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StreamSubject;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
-import com.google.common.truth.Truth8;
 import com.google.gerrit.common.Nullable;
-import java.util.Arrays;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 
@@ -43,7 +41,8 @@
   }
 
   public void hasNoMessages() {
-    Truth.assertWithMessage("expected no messages")
+    check("hasNoMessages()")
+        .withMessage("expected no messages")
         .that(Strings.nullToEmpty(trimMessages()))
         .isEqualTo("");
   }
@@ -51,14 +50,14 @@
   public void hasMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
-    Truth.assertThat(trimMessages()).isEqualTo(Arrays.stream(expectedLines).collect(joining("\n")));
+    check("messages()").that(trimMessages()).isEqualTo(String.join("\n", expectedLines));
   }
 
   public void containsMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
     Iterable<String> got = Splitter.on("\n").split(trimMessages());
-    Truth.assertThat(got).containsAllIn(expectedLines).inOrder();
+    check("messages()").that(got).containsAtLeastElementsIn(expectedLines).inOrder();
   }
 
   private String trimMessages() {
@@ -90,10 +89,7 @@
               messages, Throwables.getStackTraceAsString(e));
       return;
     }
-    Truth.assertThat(actual)
-        .named("processed commands")
-        .containsExactlyEntriesIn(expected)
-        .inOrder();
+    check("processedCommands()").that(actual).containsExactlyEntriesIn(expected).inOrder();
   }
 
   @VisibleForTesting
@@ -129,7 +125,9 @@
   }
 
   public RemoteRefUpdateSubject onlyRef(String refName) {
-    Truth8.assertThat(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
+    check("setOfRefs()")
+        .about(StreamSubject.streams())
+        .that(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
         .named("set of refs")
         .containsExactly(refName);
     return ref(refName);
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 0aa6ca2..06e44d6 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -3,12 +3,13 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov-neverlink",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index b6fb46e..9c08857 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -22,13 +22,13 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -58,7 +58,7 @@
   @Singleton
   public static class Factory {
     private final Provider<InternalAccountQuery> accountQueryProvider;
-    private final UrlFormatter urlFormatter;
+    private final DynamicItem<UrlFormatter> urlFormatter;
     private final IdentifiedUser.GenericFactory userFactory;
     private final int maxTrustDepth;
     private final ImmutableMap<Long, Fingerprint> trusted;
@@ -68,7 +68,7 @@
         @GerritServerConfig Config cfg,
         Provider<InternalAccountQuery> accountQueryProvider,
         IdentifiedUser.GenericFactory userFactory,
-        UrlFormatter urlFormatter) {
+        DynamicItem<UrlFormatter> urlFormatter) {
       this.accountQueryProvider = accountQueryProvider;
       this.urlFormatter = urlFormatter;
       this.userFactory = userFactory;
@@ -101,7 +101,7 @@
   }
 
   private final Provider<InternalAccountQuery> accountQueryProvider;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final IdentifiedUser.GenericFactory userFactory;
 
   private IdentifiedUser expectedUser;
@@ -134,7 +134,7 @@
         return checkIdsForExpectedUser(key);
       }
       return checkIdsForArbitraryUser(key);
-    } catch (PGPException | OrmException e) {
+    } catch (PGPException | RuntimeException e) {
       String msg = "Error checking user IDs for key";
       logger.atWarning().withCause(e).log("%s %s", msg, keyIdToString(key.getKeyID()));
       return CheckResult.bad(msg);
@@ -144,7 +144,7 @@
   private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException {
     Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
     if (allowedUserIds.isEmpty()) {
-      Optional<String> settings = urlFormatter.getSettingsUrl("Identities");
+      Optional<String> settings = urlFormatter.get().getSettingsUrl("Identities");
       return CheckResult.bad(
           "No identities found for user"
               + (settings.isPresent() ? "; check " + settings.get() : ""));
@@ -155,7 +155,7 @@
     return CheckResult.bad(missingUserIds(allowedUserIds));
   }
 
-  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
+  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException {
     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
     if (accountStates.isEmpty()) {
       return CheckResult.bad("Key is not associated with any users");
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 19d503f..7dd01d9 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -15,8 +15,12 @@
 package com.google.gerrit.gpg;
 
 import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -38,6 +42,8 @@
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -63,6 +69,8 @@
  * matching the ID. Multiple keys are supported because forging a key ID is possible, but such a key
  * cannot be used to verify signatures produced with the correct key.
  *
+ * <p>Subkeys are mapped to the master GPG key in the same NoteMap.
+ *
  * <p>No additional checks are performed on the key after reading; callers should only trust keys
  * after checking with a {@link PublicKeyChecker}.
  */
@@ -88,11 +96,19 @@
   public static PGPPublicKey getSigner(
       Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
     for (PGPPublicKeyRing kr : keyRings) {
-      PGPPublicKey k = kr.getPublicKey();
+      // Possibly return a signing subkey in case it differs from the master public key
+      PGPPublicKey k = kr.getPublicKey(sig.getKeyID());
+      if (k == null) {
+        throw new IllegalStateException(
+            "No public key found for ID: " + keyIdToString(sig.getKeyID()));
+      }
       sig.init(new BcPGPContentVerifierBuilderProvider(), k);
       sig.update(data);
       if (sig.verify()) {
-        return k;
+        // If the signature was made using a subkey, return the main public key.
+        // This enables further validity checks, like user ID checks, that can only
+        // be performed using the master public key.
+        return kr.getPublicKey();
       }
     }
     return null;
@@ -207,19 +223,34 @@
     if (notes == null) {
       return Collections.emptyList();
     }
-    Note note = notes.getNote(keyObjectId(keyId));
+
+    return get(keyObjectId(keyId), fp);
+  }
+
+  private List<PGPPublicKeyRing> get(ObjectId keyObjectId, byte[] fp) throws IOException {
+    Note note = notes.getNote(keyObjectId);
     if (note == null) {
       return Collections.emptyList();
     }
 
+    return readKeysFromNote(note, fp);
+  }
+
+  private List<PGPPublicKeyRing> readKeysFromNote(Note note, byte[] fp)
+      throws IOException, MissingObjectException, IncorrectObjectTypeException {
+    boolean foundAtLeastOneKey = false;
     List<PGPPublicKeyRing> keys = new ArrayList<>();
-    try (InputStream in = reader.open(note.getData(), OBJ_BLOB).openStream()) {
+    ObjectId data = note.getData();
+    try (InputStream stream = reader.open(data, OBJ_BLOB).openStream()) {
+      byte[] bytes = ByteStreams.toByteArray(stream);
+      InputStream in = new ByteArrayInputStream(bytes);
       while (true) {
         @SuppressWarnings("unchecked")
         Iterator<Object> it = new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
         if (!it.hasNext()) {
           break;
         }
+        foundAtLeastOneKey = true;
         Object obj = it.next();
         if (obj instanceof PGPPublicKeyRing) {
           PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
@@ -229,7 +260,34 @@
         }
         checkState(!it.hasNext(), "expected one PGP object per ArmoredInputStream");
       }
-      return keys;
+
+      if (foundAtLeastOneKey) {
+        return keys;
+      }
+
+      // Subkey handling
+      String id = new String(bytes, UTF_8);
+      Preconditions.checkArgument(ObjectId.isId(id), "Not valid SHA1: " + id);
+      return get(ObjectId.fromString(id), fp);
+    }
+  }
+
+  public void rebuildSubkeyMasterKeyMap()
+      throws MissingObjectException, IncorrectObjectTypeException, IOException, PGPException {
+    if (reader == null) {
+      load();
+    }
+    if (notes != null) {
+      try (ObjectInserter ins = repo.newObjectInserter()) {
+        for (Note note : notes) {
+          for (PGPPublicKeyRing keyRing :
+              new PGPPublicKeyRingCollection(readKeysFromNote(note, null))) {
+            long masterKeyId = keyRing.getPublicKey().getKeyID();
+            ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
+            saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
+          }
+        }
+      }
     }
   }
 
@@ -348,8 +406,8 @@
 
   private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
       throws PGPException, IOException {
-    long keyId = keyRing.getPublicKey().getKeyID();
-    PGPPublicKeyRingCollection existing = get(keyId);
+    long masterKeyId = keyRing.getPublicKey().getKeyID();
+    PGPPublicKeyRingCollection existing = get(masterKeyId);
     List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size() + 1);
     boolean replaced = false;
     for (PGPPublicKeyRing kr : existing) {
@@ -363,7 +421,37 @@
     if (!replaced) {
       toWrite.add(keyRing);
     }
-    notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+
+    ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
+    notes.set(masterKeyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+
+    saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
+  }
+
+  private void saveSubkeyMapping(
+      ObjectInserter ins, PGPPublicKeyRing keyRing, long masterKeyId, ObjectId masterKeyObjectId)
+      throws IOException {
+    // Subkey handling
+    byte[] masterKeyBytes = masterKeyObjectId.name().getBytes(UTF_8);
+    ObjectId masterKeyObject = null;
+    for (PGPPublicKey key : keyRing) {
+      long subKeyId = key.getKeyID();
+      // Skip master public key
+      if (masterKeyId == subKeyId) {
+        continue;
+      }
+
+      // Insert master key object only once for all subkeys
+      if (masterKeyObject == null) {
+        masterKeyObject = ins.insert(OBJ_BLOB, masterKeyBytes);
+      }
+
+      ObjectId subkeyObjectId = keyObjectId(subKeyId);
+      Preconditions.checkArgument(
+          notes.get(subkeyObjectId) == null || notes.get(subkeyObjectId).equals(masterKeyObject),
+          "Master key differs for subkey: " + subkeyObjectId.name());
+      notes.set(subkeyObjectId, masterKeyObject);
+    }
   }
 
   private void deleteFromNotes(ObjectInserter ins, Fingerprint fp)
@@ -378,10 +466,24 @@
     }
     if (toWrite.size() == existing.size()) {
       return;
-    } else if (!toWrite.isEmpty()) {
-      notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+    }
+
+    ObjectId keyObjectId = keyObjectId(keyId);
+    if (!toWrite.isEmpty()) {
+      notes.set(keyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
     } else {
-      notes.remove(keyObjectId(keyId));
+      PGPPublicKeyRing keyRing = get(fp.get());
+
+      for (PGPPublicKey key : keyRing) {
+        long subKeyId = key.getKeyID();
+        // Skip master public key
+        if (keyId == subKeyId) {
+          continue;
+        }
+        notes.remove(keyObjectId(subKeyId));
+      }
+
+      notes.remove(keyObjectId);
     }
   }
 
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 967259a..b7b03db 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GpgApiAdapter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -67,7 +66,7 @@
       throws RestApiException, GpgException {
     try {
       return gpgKeys.get().list().apply(account);
-    } catch (OrmException | PGPException | IOException e) {
+    } catch (PGPException | IOException e) {
       throw new GpgException(e);
     }
   }
@@ -81,7 +80,7 @@
     in.delete = delete;
     try {
       return postGpgKeys.get().apply(account, in);
-    } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (PGPException | IOException | ConfigInvalidException e) {
       throw new GpgException(e);
     }
   }
@@ -91,7 +90,7 @@
       throws RestApiException, GpgException {
     try {
       return gpgKeyApiFactory.create(gpgKeys.get().parse(account, idStr));
-    } catch (PGPException | OrmException | IOException e) {
+    } catch (PGPException | IOException e) {
       throw new GpgException(e);
     }
   }
diff --git a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index 25b472d..cf09acf 100644
--- a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.gpg.server.DeleteGpgKey;
 import com.google.gerrit.gpg.server.GpgKey;
 import com.google.gerrit.gpg.server.GpgKeys;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -57,7 +56,7 @@
   public void delete() throws RestApiException {
     try {
       delete.apply(rsrc, new Input());
-    } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (PGPException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot delete GPG key", e);
     }
   }
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index a636a8b..69e106c 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -63,7 +62,7 @@
 
   @Override
   public Response<?> apply(GpgKey rsrc, Input input)
-      throws RestApiException, PGPException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, PGPException, IOException, ConfigInvalidException {
     PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
     String fingerprint = BaseEncoding.base16().encode(key.getFingerprint());
     Optional<ExternalId> extId = externalIds.get(ExternalId.Key.create(SCHEME_GPGKEY, fingerprint));
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index 3f090a1..16592f8 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -86,7 +85,7 @@
 
   @Override
   public GpgKey parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, PGPException, OrmException, IOException {
+      throws ResourceNotFoundException, PGPException, IOException {
     checkVisible(self, parent);
 
     ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
@@ -142,7 +141,7 @@
   public class ListGpgKeys implements RestReadView<AccountResource> {
     @Override
     public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
-        throws OrmException, PGPException, IOException, ResourceNotFoundException {
+        throws PGPException, IOException, ResourceNotFoundException {
       checkVisible(self, rsrc);
       Map<String, GpgKeyInfo> keys = new HashMap<>();
       try (PublicKeyStore store = storeProvider.get()) {
@@ -219,13 +218,14 @@
       Iterator<String> userIds = key.getUserIDs();
       info.userIds = ImmutableList.copyOf(userIds);
 
-      try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
-          ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
-        // This is not exactly the key stored in the store, but is equivalent. In
-        // particular, it will have a Bouncy Castle version string. The armored
-        // stream reader in PublicKeyStore doesn't give us an easy way to extract
-        // the original ASCII armor.
-        key.encode(aout);
+      try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) {
+        try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
+          // This is not exactly the key stored in the store, but is equivalent. In
+          // particular, it will have a Bouncy Castle version string. The armored
+          // stream reader in PublicKeyStore doesn't give us an easy way to extract
+          // the original ASCII armor.
+          key.encode(aout);
+        }
         info.key = new String(out.toByteArray(), UTF_8);
       }
     }
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 7d08fca..57c95a9 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -26,7 +26,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -50,7 +50,6 @@
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.AddKeySender;
 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;
@@ -108,7 +107,7 @@
   @Override
   public Map<String, GpgKeyInfo> apply(AccountResource rsrc, GpgKeysInput input)
       throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
-          PGPException, OrmException, IOException, ConfigInvalidException {
+          PGPException, IOException, ConfigInvalidException {
     GpgKeys.checkVisible(self, rsrc);
 
     Collection<ExternalId> existingExtIds =
@@ -249,7 +248,7 @@
     return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
   }
 
-  private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
+  private Account getAccountByExternalId(ExternalId.Key extIdKey) {
     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
 
     if (accountStates.isEmpty()) {
diff --git a/java/com/google/gerrit/gpg/testing/TestKeys.java b/java/com/google/gerrit/gpg/testing/TestKeys.java
index 00acedb..de66889 100644
--- a/java/com/google/gerrit/gpg/testing/TestKeys.java
+++ b/java/com/google/gerrit/gpg/testing/TestKeys.java
@@ -1029,4 +1029,81 @@
             + "=JxsF\n"
             + "-----END PGP PRIVATE KEY BLOCK-----\n");
   }
+
+  /**
+   * Master Key without expiration with subkey with expiration.
+   *
+   * <pre>
+   * pub   rsa1024 2018-11-17 [C]
+   *       5734 2C37 982A 843B 19C0  622B 6AAF 2D26 B481 02DB
+   * uid            [ultimate] Testuser 10 <testuser10@example.com>
+   * sub   rsa1024 2018-11-17 [S] [expires: 2065-11-05]
+   *       0A4A 9660 1B96 2DFC E898  E686 4305 C92E 626E B485
+   * </pre>
+   */
+  public static TestKey validKeyWithoutExpirationWithSubkeyWithExpiration() throws Exception {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+            + "\n"
+            + "mI0EW+98GgEEALLn87xX++daic3AzKwM7nY50Mx2eTaEZPlnaDAVvFlhbZuPG+n5\n"
+            + "g93vYX3wEfnFxI7IBEe7VMT1AyszLZgpFmbzW8eGQxCGpRd1hYrUUlC0IkGAwG9v\n"
+            + "LQB85GDDZUUH4p+A4oHX0yUm8iCpbO9+D82xzNDe/D8Xbw1foWMWGonLABEBAAG0\n"
+            + "JFRlc3R1c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEE\n"
+            + "VzQsN5gqhDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwEC\n"
+            + "HgECF4AACgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5v\n"
+            + "jpAjwusIHRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7\n"
+            + "fDgfqnNa5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/\n"
+            + "8v7ABV24jQRb73xXAQQAssv5gwxWx5J0q4gGcqMIaJKzBaHAjiK3ryH6qnFQpsf1\n"
+            + "ODtU+a4NxFJsXGOd6jHEhBEHPgWAaaKZ7PEJVnwA/XOhPG+q9YimAbbZS0qmC/LH\n"
+            + "DpFtFbsJsMKZbIC69j9OcbmalIowspFQBVeAankGFReZVhh99Z/o81Y+Twm9eisA\n"
+            + "EQEAAYkBcQQYAQgAJhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlY\n"
+            + "WHSAAL8JEGqvLSa0gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUC\n"
+            + "W+98VwAKCRBDBckuYm60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oup\n"
+            + "TimR1cVgN1CEAWh82UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQK\n"
+            + "mXGWM7Q33hU1E7IfgU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuo\n"
+            + "qRwqjHt2BACuXwA8RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Ws\n"
+            + "qdx2vbbJxy1bfTYxb0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOy\n"
+            + "ziatLaUBSQfyG656lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
+            + "=BG9Z\n"
+            + "-----END PGP PUBLIC KEY BLOCK-----",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+            + "\n"
+            + "lQIGBFvvfBoBBACy5/O8V/vnWonNwMysDO52OdDMdnk2hGT5Z2gwFbxZYW2bjxvp\n"
+            + "+YPd72F98BH5xcSOyARHu1TE9QMrMy2YKRZm81vHhkMQhqUXdYWK1FJQtCJBgMBv\n"
+            + "by0AfORgw2VFB+KfgOKB19MlJvIgqWzvfg/NsczQ3vw/F28NX6FjFhqJywARAQAB\n"
+            + "/gcDAmvJS+mrsxlf7D58lnhHw8gBjP5JkY9anTgVhIdieJEIlitV4PyRQYPAGZZd\n"
+            + "asUC7bC7WnLCygiU59kXS5z63Ue/RM0tVwWy0FsigpseC90Mtwb4wjL2jTeebszD\n"
+            + "UcM33d6Tg9s4eNnsHzpmlC/CReW6MYJj0/06AsvgUgOxgWXf0YapOLRIr60reTUb\n"
+            + "ovVZtH76rsZXyQvR9qJv11F+BmIDDzg4EsipXDGVuEZ0SXJyq6OLAUPkV2ZdELaT\n"
+            + "P4RWp0Zsn22H8jm4MsZ7la2Ux3jD2AMdy2B9dpwhuxOegDSXUMRfXYwxQ1wioqpA\n"
+            + "pOZ1RjjFsID34XNtxGp3wMYcFleOl3CSpXyW/P1PYTVBQta/Y7xniEetzUk9NHVc\n"
+            + "2jMD8a8767+Tk0SChIPmWOhQYrHS1Ce309SjTRSRVexjKF0Mp5WXHxqz582IlPT9\n"
+            + "jdxvLjVIW4xbtZmnQ2JnPHInWbxoa3exaoPg5osvg8h7QlGxRY6H2/20JFRlc3R1\n"
+            + "c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEEVzQsN5gq\n"
+            + "hDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA\n"
+            + "CgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5vjpAjwusI\n"
+            + "HRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7fDgfqnNa\n"
+            + "5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/8v7ABV2d\n"
+            + "AgYEW+98VwEEALLL+YMMVseSdKuIBnKjCGiSswWhwI4it68h+qpxUKbH9Tg7VPmu\n"
+            + "DcRSbFxjneoxxIQRBz4FgGmimezxCVZ8AP1zoTxvqvWIpgG22UtKpgvyxw6RbRW7\n"
+            + "CbDCmWyAuvY/TnG5mpSKMLKRUAVXgGp5BhUXmVYYffWf6PNWPk8JvXorABEBAAH+\n"
+            + "BwMClgBcY/ItnafslgEWZOSqsxCSJatN72c9zzWZE+zmcx9NbDRuCVxXhTbHJZZs\n"
+            + "Hz44vsKqOKtyhrfr9Oke0mYyzH2CX6tv6ghJyC6znRCGoc/P84uJ1v3ibO/7/p85\n"
+            + "PDHzEEpHmdbef+UymnjZBYKGi45SINy3bLwOa/Vl80Q4wsppPe9oynerq6ig94HR\n"
+            + "e3mNDw/JggtgJA0X2VmmmXG8vHwIB5EziQrH7QGtLyjqdE+w7CLbbvAskL8Uw1qx\n"
+            + "Aowdpb7J8hrUdIDDCr/mlhT17+UM5yOXHKcixyrscqbjlG/nqwPvR10efo7D0rFR\n"
+            + "6tu5OU2y3N2PhGOysDLgupUXBLlpdByF6AYNV9zvU7ipO7QXzrUfYCb/WyAcjl+X\n"
+            + "Yl38sCVTVFGsB2ql9/fzFCxAB3FUNHDlI2sUbkdDPcjgf65SK0GGcckWfntfq9dj\n"
+            + "pQzEVen8X9dT3UhfuvHd98g3n6ju9gh8NucwHM5jITq9ItTY0whb+okBcQQYAQgA\n"
+            + "JhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlYWHSAAL8JEGqvLSa0\n"
+            + "gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUCW+98VwAKCRBDBcku\n"
+            + "Ym60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oupTimR1cVgN1CEAWh8\n"
+            + "2UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQKmXGWM7Q33hU1E7If\n"
+            + "gU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuoqRwqjHt2BACuXwA8\n"
+            + "RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Wsqdx2vbbJxy1bfTYx\n"
+            + "b0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOyziatLaUBSQfyG656\n"
+            + "lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
+            + "=RbPm\n"
+            + "-----END PGP PRIVATE KEY BLOCK-----");
+  }
 }
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index def4b30..5ed8169 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -10,6 +10,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/json",
@@ -30,7 +31,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
         "//lib:soy",
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 6a19be7..bbe15b5 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
@@ -96,7 +95,7 @@
   private void authFromCookie(String cookie) {
     key = new Key(cookie);
     val = manager.get(key);
-    String token = request.getHeader(HostPageData.XSRF_HEADER_NAME);
+    String token = request.getHeader(XsrfConstants.XSRF_HEADER_NAME);
     if (val != null && token != null && token.equals(val.getAuth())) {
       okPaths.add(AccessPath.REST_API);
     }
diff --git a/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index ac66845..d13f2f6 100644
--- a/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -17,9 +17,12 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Strings.emptyToNull;
 import static com.google.common.net.HttpHeaders.AUTHORIZATION;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.CONTENTTYPE_VND_GIT_LFS_JSON;
+import static com.google.gerrit.httpd.GerritAuthModule.NOT_AUTHORIZED_LFS_URL_REGEX;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.AccessPath;
@@ -32,6 +35,7 @@
 import java.io.IOException;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -55,6 +59,9 @@
  */
 @Singleton
 class ContainerAuthFilter implements Filter {
+  private static final String LFS_AUTH_PREFIX = "Ssh: ";
+  private static final Pattern LFS_ENDPOINT = Pattern.compile(NOT_AUTHORIZED_LFS_URL_REGEX);
+
   private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
@@ -93,6 +100,11 @@
   private boolean verify(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     String username = RemoteUserUtil.getRemoteUser(req, loginHttpHeader);
     if (username == null) {
+      if (isLfsOverSshRequest(req)) {
+        // LFS-over-SSH auth request cannot be authorized by container
+        // therefore let it go through the filter
+        return true;
+      }
       rsp.sendError(SC_FORBIDDEN);
       return false;
     }
@@ -111,4 +123,12 @@
     ws.setAccessPathOk(AccessPath.REST_API, true);
     return true;
   }
+
+  private static boolean isLfsOverSshRequest(HttpServletRequest req) {
+    String hdr = req.getHeader(AUTHORIZATION);
+    return CONTENTTYPE_VND_GIT_LFS_JSON.equals(req.getContentType())
+        && !Strings.isNullOrEmpty(hdr)
+        && hdr.startsWith(LFS_AUTH_PREFIX)
+        && LFS_ENDPOINT.matcher(req.getRequestURI()).matches();
+  }
 }
diff --git a/java/com/google/gerrit/httpd/GerritAuthModule.java b/java/com/google/gerrit/httpd/GerritAuthModule.java
index c0ef207..253c220 100644
--- a/java/com/google/gerrit/httpd/GerritAuthModule.java
+++ b/java/com/google/gerrit/httpd/GerritAuthModule.java
@@ -24,7 +24,7 @@
 
 /** Configures filter for authenticating REST requests. */
 public class GerritAuthModule extends ServletModule {
-  private static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
+  static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
   private final AuthConfig authConfig;
 
   @Inject
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 7fbb8d7..c97ee10 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -54,6 +55,7 @@
 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;
@@ -129,6 +131,25 @@
                   .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();
     }
   }
 
@@ -317,22 +338,26 @@
     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);
@@ -431,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);
diff --git a/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index ab7bfdf..1eaaba3 100644
--- a/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -38,14 +38,14 @@
   private final DynamicItem<WebSession> webSession;
   private final Provider<String> urlProvider;
   private final String logoutUrl;
-  private final AuditService audit;
+  private final GroupAuditService audit;
 
   @Inject
   protected HttpLogoutServlet(
       AuthConfig authConfig,
       DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      AuditService audit) {
+      GroupAuditService audit) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.logoutUrl = authConfig.getLogoutURL();
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
index 164f957..d53a5c5 100644
--- a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -21,7 +21,6 @@
 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;
@@ -61,7 +60,7 @@
     } catch (ResourceConflictException | ResourceNotFoundException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
-    } catch (OrmException | PermissionBackendException e) {
+    } catch (PermissionBackendException | RuntimeException e) {
       throw new IOException("Unable to lookup change " + id.id, e);
     }
     String path =
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index f3bf5af..15dbcab 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,6 +21,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.ServletModule;
@@ -103,19 +103,18 @@
         return;
       }
 
-      Account target;
+      Account.Id target;
       try {
-        target = accountResolver.find(runas);
-      } catch (OrmException | IOException | ConfigInvalidException e) {
+        target = accountResolver.resolve(runas).asUnique().getAccount().getId();
+      } catch (UnprocessableEntityException e) {
+        replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
+        return;
+      } catch (IOException | ConfigInvalidException | RuntimeException e) {
         logger.atWarning().withCause(e).log("cannot resolve account for %s", RUN_AS);
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
         return;
       }
-      if (target == null) {
-        replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
-        return;
-      }
-      session.get().setUserAccountId(target.getId());
+      session.get().setUserAccountId(target);
     }
 
     chain.doFilter(req, res);
diff --git a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
index 6e32980..107a07e 100644
--- a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
+++ b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
@@ -87,7 +87,7 @@
   }
 
   private Optional<IdentifiedUser> loggedInUser() {
-    return session.call(s -> s.isSignedIn())
+    return session.call(WebSession::isSignedIn)
         ? Optional.of(userProvider.get().asIdentifiedUser())
         : Optional.empty();
   }
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index cb1e965..d09b4dd 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -215,7 +215,15 @@
       return expiresAt;
     }
 
-    Account.Id getAccountId() {
+    /**
+     * Parse an Account.Id.
+     *
+     * <p>This is public so that plugins that implement a web session, can also implement a way to
+     * clear per user sessions.
+     *
+     * @return account ID.
+     */
+    public Account.Id getAccountId() {
       return accountId;
     }
 
diff --git a/java/com/google/gerrit/common/data/HostPageData.java b/java/com/google/gerrit/httpd/XsrfConstants.java
similarity index 93%
rename from java/com/google/gerrit/common/data/HostPageData.java
rename to java/com/google/gerrit/httpd/XsrfConstants.java
index bf58ff4..0bbfe34 100644
--- a/java/com/google/gerrit/common/data/HostPageData.java
+++ b/java/com/google/gerrit/httpd/XsrfConstants.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.httpd;
 
 /** XSRF Constants. */
-public class HostPageData {
+public class XsrfConstants {
   /**
    * Name of the cookie in which the XSRF token is sent from the server to the client during host
    * page bootstrapping.
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 d6ff2ec..552e667 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -188,33 +187,20 @@
   }
 
   private AuthResult byUserName(String userName) {
-    try {
-      List<AccountState> accountStates =
-          queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
-      if (accountStates.isEmpty()) {
-        getServletContext().log("No accounts with username " + userName + " found");
-        return null;
-      }
-      if (accountStates.size() > 1) {
-        getServletContext().log("Multiple accounts with username " + userName + " found");
-        return null;
-      }
-      return auth(accountStates.get(0).getAccount().getId());
-    } catch (OrmException e) {
-      getServletContext().log("cannot query account index", e);
+    List<AccountState> accountStates = queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
+    if (accountStates.isEmpty()) {
+      getServletContext().log("No accounts with username " + userName + " found");
       return null;
     }
+    if (accountStates.size() > 1) {
+      getServletContext().log("Multiple accounts with username " + userName + " found");
+      return null;
+    }
+    return auth(accountStates.get(0).getAccount().getId());
   }
 
   private Optional<AuthResult> byPreferredEmail(String email) {
-    try {
-      Optional<AccountState> match =
-          queryProvider.get().byPreferredEmail(email).stream().findFirst();
-      return auth(match);
-    } catch (OrmException e) {
-      getServletContext().log("cannot query database", e);
-      return Optional.empty();
-    }
+    return auth(queryProvider.get().byPreferredEmail(email).stream().findFirst());
   }
 
   private Optional<AuthResult> byAccountId(String idStr) {
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index fd2f628..1b7e477 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.CacheHeaders;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -128,7 +127,7 @@
         logger.atFine().log(
             "Associating external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         updateRemoteExternalId(arsp, remoteExternalId);
-      } catch (AccountException | OrmException | ConfigInvalidException e) {
+      } catch (AccountException | ConfigInvalidException e) {
         logger.atSevere().withCause(e).log(
             "Unable to associate external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
@@ -152,7 +151,7 @@
   }
 
   private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     accountManager.updateLink(
         arsp.getAccountId(),
         new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, remoteAuthToken)));
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index 7315ce1..dd3e5fc 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
@@ -13,7 +14,6 @@
         "//java/com/google/gerrit/server/audit",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
index d25ff60..f468ecb 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HttpLogoutServlet;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,7 +39,7 @@
       AuthConfig authConfig,
       DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      AuditService audit,
+      GroupAuditService audit,
       Provider<OAuthSession> oauthSession) {
     super(authConfig, webSession, urlProvider, audit);
     this.oauthSession = oauthSession;
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index b780fa0..0c8a1a1 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -188,7 +187,7 @@
       logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get().toString());
       try {
         accountManager.link(claimedId.get(), req);
-      } catch (OrmException | ConfigInvalidException e) {
+      } catch (ConfigInvalidException e) {
         logger.atSevere().log(
             "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
             user.getExternalId(), claimedId.get(), claimedIdentifier);
@@ -203,7 +202,7 @@
       throws AccountException, IOException {
     try {
       accountManager.link(identifiedUser.get().getAccountId(), areq);
-    } catch (OrmException | ConfigInvalidException e) {
+    } catch (ConfigInvalidException e) {
       logger.atSevere().log(
           "Cannot link: %s to user identity: %s",
           user.getExternalId(), identifiedUser.get().getAccountId());
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index f80e9d5..f09f93d 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -8,6 +8,7 @@
         # We want all these deps to be provided_deps
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
@@ -15,7 +16,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/audit",
         "//lib:guava",
-        "//lib:gwtorm",
+        "//java/com/google/gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
index 8299c16..d75805c 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HttpLogoutServlet;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,7 +39,7 @@
       AuthConfig authConfig,
       DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      AuditService audit,
+      GroupAuditService audit,
       Provider<OAuthSessionOverOpenID> oauthSession) {
     super(authConfig, webSession, urlProvider, audit);
     this.oauthSession = oauthSession;
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index a51a0ab..08f2d52 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -164,7 +163,7 @@
           logger.atFine().log("Claimed account already exists: link to it.");
           try {
             accountManager.link(claimedId.get(), areq);
-          } catch (OrmException | ConfigInvalidException e) {
+          } catch (ConfigInvalidException e) {
             logger.atSevere().log(
                 "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
                 user.getExternalId(), claimedId.get(), claimedIdentifier);
@@ -178,7 +177,7 @@
         try {
           logger.atFine().log("Linking \"%s\" to \"%s\"", user.getExternalId(), accountId);
           accountManager.link(accountId, areq);
-        } catch (OrmException | ConfigInvalidException e) {
+        } catch (ConfigInvalidException e) {
           logger.atSevere().log(
               "Cannot link: %s to user identity: %s", user.getExternalId(), accountId);
           rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
diff --git a/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java b/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
index 2f7f4bd..864b160 100644
--- a/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
+++ b/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package com.google.gerrit.httpd.auth.openid;
 
 import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index d557c0e..df072b2 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -26,7 +26,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/flogger:api",
         "//lib/guice",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index a8552f4..aa5362b 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -66,9 +66,9 @@
 import com.google.gerrit.server.config.SysExecutorModule;
 import com.google.gerrit.server.events.EventBroker;
 import com.google.gerrit.server.events.StreamEventsApiListener;
-import com.google.gerrit.server.git.ChangeRefCache;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
@@ -288,7 +288,7 @@
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new GerritApiModule());
     modules.add(new PluginApiModule());
-    modules.add(new ChangeRefCache.Module());
+    modules.add(new SearchingChangeCacheImpl.Module());
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultMemoryCacheModule());
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/raw/CatServlet.java b/java/com/google/gerrit/httpd/raw/CatServlet.java
index 1e60d31..1d0e7d8b 100644
--- a/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -145,7 +144,7 @@
     } catch (ResourceConflictException | NoSuchChangeException | AuthException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
-    } catch (OrmException | PermissionBackendException | IOException e) {
+    } catch (PermissionBackendException | IOException e) {
       getServletContext().log("Cannot query database", e);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       return;
diff --git a/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 2f4d7b2..8be4abc 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -33,8 +33,8 @@
 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.gerrit.util.http.RequestUtil;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/httpd/raw/ToolServlet.java b/java/com/google/gerrit/httpd/raw/ToolServlet.java
index 2f522a2..0d707a6 100644
--- a/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -25,7 +25,6 @@
 
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.tools.ToolsCatalog;
-import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.util.http.RequestUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -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 += "/";
       }
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
index 61132e7..a3aba6d 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.quota.QuotaBackend;
 import com.google.gerrit.server.quota.QuotaException;
 import com.google.gerrit.util.http.RequestUtil;
-import javax.inject.Inject;
+import com.google.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 
 /**
@@ -32,7 +32,7 @@
  * <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 => /changes/reviewed:PUT
+ *   <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
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 4057d54..d6dca48 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -105,10 +105,10 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
-import com.google.gerrit.server.audit.AuditService;
 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.group.GroupAuditService;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -225,7 +225,7 @@
     final DynamicItem<WebSession> webSession;
     final Provider<ParameterParser> paramParser;
     final PermissionBackend permissionBackend;
-    final AuditService auditService;
+    final GroupAuditService auditService;
     final RestApiMetrics metrics;
     final Pattern allowOrigin;
     final RestApiQuotaEnforcer quotaChecker;
@@ -236,7 +236,7 @@
         DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
         PermissionBackend permissionBackend,
-        AuditService auditService,
+        GroupAuditService auditService,
         RestApiMetrics metrics,
         RestApiQuotaEnforcer quotaChecker,
         @GerritServerConfig Config cfg) {
@@ -392,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 {
@@ -401,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 {
@@ -1264,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);
@@ -1277,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());
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index c927b95..dfdc014 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -1,5 +1,3 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
-
 QUERY_PARSE_EXCEPTION_SRCS = [
     "query/QueryParseException.java",
     "query/QueryRequiresAuthException.java",
@@ -22,13 +20,13 @@
         ":query_exception",
         "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/antlr:java-runtime",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/index/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index beb9c07..fb48104 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -18,7 +18,8 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.CharMatcher;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import java.io.IOException;
 import java.sql.Timestamp;
 
@@ -60,7 +61,8 @@
 
   @FunctionalInterface
   public interface Getter<I, T> {
-    T get(I input) throws OrmException, IOException;
+    @Nullable
+    T get(I input) throws IOException;
   }
 
   public static class Builder<T> {
@@ -131,13 +133,13 @@
    *
    * @param input input object.
    * @return the field value(s) to index.
-   * @throws OrmException
    */
-  public T get(I input) throws OrmException {
+  @Nullable
+  public T get(I input) {
     try {
       return getter.get(input);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index f60c08f..44f8b42 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.index;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Optional;
 
 /**
@@ -48,24 +47,18 @@
    * searchers, but should be visible within a reasonable amount of time.
    *
    * @param obj document object
-   * @throws IOException
    */
-  void replace(V obj) throws IOException;
+  void replace(V obj);
 
   /**
    * Delete a document from the index by key.
    *
    * @param key document key
-   * @throws IOException
    */
-  void delete(K key) throws IOException;
+  void delete(K key);
 
-  /**
-   * Delete all documents from the index.
-   *
-   * @throws IOException
-   */
-  void deleteAll() throws IOException;
+  /** Delete all documents from the index. */
+  void deleteAll();
 
   /**
    * Convert the given operator predicate into a source searching the index and returning only the
@@ -91,20 +84,17 @@
    * @param opts query options. Options that do not make sense in the context of a single document,
    *     such as start, will be ignored.
    * @return a single document if present.
-   * @throws IOException
    */
-  default Optional<V> get(K key, QueryOptions opts) throws IOException {
+  default Optional<V> get(K key, QueryOptions opts) {
     opts = opts.withStart(0).withLimit(2);
     ImmutableList<V> results;
     try {
       results = getSource(keyPredicate(key), opts).read().toList();
     } catch (QueryParseException e) {
-      throw new IOException("Unexpected QueryParseException during get()", e);
-    } catch (OrmException e) {
-      throw new IOException(e);
+      throw new StorageException("Unexpected QueryParseException during get()", e);
     }
     if (results.size() > 1) {
-      throw new IOException("Multiple results found in index for key " + key + ": " + results);
+      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
     }
     return results.stream().findFirst();
   }
@@ -116,20 +106,17 @@
    * @param opts query options. Options that do not make sense in the context of a single document,
    *     such as start, will be ignored.
    * @return an abstraction of a raw index document to retrieve fields from.
-   * @throws IOException
    */
-  default Optional<FieldBundle> getRaw(K key, QueryOptions opts) throws IOException {
+  default Optional<FieldBundle> getRaw(K key, QueryOptions opts) {
     opts = opts.withStart(0).withLimit(2);
     ImmutableList<FieldBundle> results;
     try {
       results = getSource(keyPredicate(key), opts).readRaw().toList();
     } catch (QueryParseException e) {
-      throw new IOException("Unexpected QueryParseException during get()", e);
-    } catch (OrmException e) {
-      throw new IOException(e);
+      throw new StorageException("Unexpected QueryParseException during get()", e);
     }
     if (results.size() > 1) {
-      throw new IOException("Multiple results found in index for key " + key + ": " + results);
+      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
     }
     return results.stream().findFirst();
   }
@@ -146,7 +133,6 @@
    * Mark whether this index is up-to-date and ready to serve reads.
    *
    * @param ready whether the index is ready
-   * @throws IOException
    */
-  void markReady(boolean ready) throws IOException;
+  void markReady(boolean ready);
 }
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 2b3c63e..e633bfa 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -179,7 +178,7 @@
               Object v;
               try {
                 v = f.get(obj);
-              } catch (OrmException e) {
+              } catch (RuntimeException e) {
                 logger.atSevere().withCause(e).log(
                     "error getting field %s of %s", f.getName(), obj);
                 return null;
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 5e484b2..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
@@ -61,9 +61,7 @@
       storedOnly("ref_state")
           .buildRepeatable(
               projectData ->
-                  projectData
-                      .tree()
-                      .stream()
+                  projectData.tree().stream()
                       .filter(p -> p.getProject().getConfigRefState() != null)
                       .map(p -> toRefState(p.getProject()))
                       .collect(toImmutableList()));
diff --git a/java/com/google/gerrit/index/project/ProjectIndexer.java b/java/com/google/gerrit/index/project/ProjectIndexer.java
index 44dccfe..1ca29f5 100644
--- a/java/com/google/gerrit/index/project/ProjectIndexer.java
+++ b/java/com/google/gerrit/index/project/ProjectIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.index.project;
 
 import com.google.gerrit.reviewdb.client.Project;
-import java.io.IOException;
 
 public interface ProjectIndexer {
 
@@ -24,5 +23,5 @@
    *
    * @param nameKey name key of project to index.
    */
-  void index(Project.NameKey nameKey) throws IOException;
+  void index(Project.NameKey nameKey);
 }
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 7fba05f..ae13fb3 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -82,7 +81,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     for (Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 649dc32..7d817d2 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -17,12 +17,10 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
+import com.google.gerrit.exceptions.StorageException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -76,26 +74,9 @@
   }
 
   @Override
-  public ResultSet<T> read() throws OrmException {
-    try {
-      return readImpl();
-    } catch (OrmRuntimeException err) {
-      if (err.getCause() != null) {
-        Throwables.throwIfInstanceOf(err.getCause(), OrmException.class);
-      }
-      throw new OrmException(err);
-    }
-  }
-
-  @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
-    // TOOD(hiesel): Implement
-    throw new UnsupportedOperationException("not implemented");
-  }
-
-  private ResultSet<T> readImpl() throws OrmException {
+  public ResultSet<T> read() {
     if (source == null) {
-      throw new OrmException("No DataSource: " + this);
+      throw new StorageException("No DataSource: " + this);
     }
     List<T> r = new ArrayList<>();
     T last = null;
@@ -142,12 +123,18 @@
   }
 
   @Override
+  public ResultSet<FieldBundle> readRaw() {
+    // TOOD(hiesel): Implement
+    throw new UnsupportedOperationException("not implemented");
+  }
+
+  @Override
   public boolean isMatchable() {
     return isVisibleToPredicate != null || super.isMatchable();
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     if (isVisibleToPredicate != null && !isVisibleToPredicate.match(object)) {
       return false;
     }
@@ -164,7 +151,7 @@
         .transformAndConcat(this::transformBuffer);
   }
 
-  protected List<T> transformBuffer(List<T> buffer) throws OrmRuntimeException {
+  protected List<T> transformBuffer(List<T> buffer) {
     return buffer;
   }
 
diff --git a/java/com/google/gerrit/index/query/DataSource.java b/java/com/google/gerrit/index/query/DataSource.java
index a82337f..2c2ba53 100644
--- a/java/com/google/gerrit/index/query/DataSource.java
+++ b/java/com/google/gerrit/index/query/DataSource.java
@@ -14,15 +14,13 @@
 
 package com.google.gerrit.index.query;
 
-import com.google.gwtorm.server.OrmException;
-
 public interface DataSource<T> {
   /** @return an estimate of the number of results from {@link #read()}. */
   int getCardinality();
 
   /** @return read from the database and return the results. */
-  ResultSet<T> read() throws OrmException;
+  ResultSet<T> read();
 
   /** @return read from the database and return the raw results. */
-  ResultSet<FieldBundle> readRaw() throws OrmException;
+  ResultSet<FieldBundle> readRaw();
 }
diff --git a/java/com/google/gerrit/index/query/IndexedQuery.java b/java/com/google/gerrit/index/query/IndexedQuery.java
index 8df46a7..d9e33ea 100644
--- a/java/com/google/gerrit/index/query/IndexedQuery.java
+++ b/java/com/google/gerrit/index/query/IndexedQuery.java
@@ -16,9 +16,9 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.QueryOptions;
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.List;
 
@@ -77,17 +77,17 @@
   }
 
   @Override
-  public ResultSet<T> read() throws OrmException {
+  public ResultSet<T> read() {
     return source.read();
   }
 
   @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
+  public ResultSet<FieldBundle> readRaw() {
     return source.readRaw();
   }
 
   @Override
-  public ResultSet<T> restart(int start) throws OrmException {
+  public ResultSet<T> restart(int start) {
     opts = opts.withStart(start);
     try {
       source = index.getSource(pred, opts);
@@ -95,7 +95,7 @@
       // 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);
+      throw new StorageException(e);
     }
     // Don't convert start to a limit, since the caller of this method (see
     // AndSource) has calculated the actual number to skip.
diff --git a/java/com/google/gerrit/index/query/IntegerRangePredicate.java b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
index 66351a8..6780867 100644
--- a/java/com/google/gerrit/index/query/IntegerRangePredicate.java
+++ b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.RangeUtil.Range;
-import com.google.gwtorm.server.OrmException;
 
 public abstract class IntegerRangePredicate<T> extends IndexPredicate<T> {
   private final Range range;
@@ -30,9 +29,9 @@
     }
   }
 
-  protected abstract Integer getValueInt(T object) throws OrmException;
+  protected abstract Integer getValueInt(T object);
 
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     Integer valueInt = getValueInt(object);
     if (valueInt == null) {
       return false;
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index ca5cc9b..48e214e 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -20,12 +20,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
-import com.google.gwtorm.server.OrmException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Supplier;
@@ -88,15 +88,15 @@
     return self();
   }
 
-  public final List<T> query(Predicate<T> p) throws OrmException {
+  public final List<T> query(Predicate<T> p) {
     return queryResults(p).entities();
   }
 
-  final QueryResult<T> queryResults(Predicate<T> p) throws OrmException {
+  final QueryResult<T> queryResults(Predicate<T> p) {
     try {
       return queryProcessor.query(p);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -110,11 +110,11 @@
    * @return results of the queries, one list of results per input query, in the same order as the
    *     input.
    */
-  public final List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
+  public final List<List<T>> query(List<Predicate<T>> queries) {
     try {
       return Lists.transform(queryProcessor.query(queries), QueryResult::entities);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -144,11 +144,9 @@
    * @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 {
+      Supplier<? extends InternalQuery<T, ?>> querySupplier, Predicate<T> predicate) {
     ImmutableList.Builder<T> b = null;
     int start = 0;
     while (true) {
diff --git a/java/com/google/gerrit/index/query/Matchable.java b/java/com/google/gerrit/index/query/Matchable.java
index 3d07943..7a16ae8 100644
--- a/java/com/google/gerrit/index/query/Matchable.java
+++ b/java/com/google/gerrit/index/query/Matchable.java
@@ -14,15 +14,9 @@
 
 package com.google.gerrit.index.query;
 
-import com.google.gwtorm.server.OrmException;
-
 public interface Matchable<T> {
-  /**
-   * Does this predicate match this object?
-   *
-   * @throws OrmException
-   */
-  boolean match(T object) throws OrmException;
+  /** Does this predicate match this object? */
+  boolean match(T object);
 
   /** @return a cost estimate to run this predicate, higher figures cost more. */
   int getCost();
diff --git a/java/com/google/gerrit/index/query/NotPredicate.java b/java/com/google/gerrit/index/query/NotPredicate.java
index 750759d..14cb740 100644
--- a/java/com/google/gerrit/index/query/NotPredicate.java
+++ b/java/com/google/gerrit/index/query/NotPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -64,7 +63,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     checkState(
         that.isMatchable(),
         "match invoked, but child predicate %s doesn't implement %s",
diff --git a/java/com/google/gerrit/index/query/OrPredicate.java b/java/com/google/gerrit/index/query/OrPredicate.java
index 8c3ed1c..9bc3769 100644
--- a/java/com/google/gerrit/index/query/OrPredicate.java
+++ b/java/com/google/gerrit/index/query/OrPredicate.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -82,7 +81,7 @@
   }
 
   @Override
-  public boolean match(T object) throws OrmException {
+  public boolean match(T object) {
     for (Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
diff --git a/java/com/google/gerrit/index/query/Paginated.java b/java/com/google/gerrit/index/query/Paginated.java
index c11d8c7..e61dd53 100644
--- a/java/com/google/gerrit/index/query/Paginated.java
+++ b/java/com/google/gerrit/index/query/Paginated.java
@@ -15,10 +15,9 @@
 package com.google.gerrit.index.query;
 
 import com.google.gerrit.index.QueryOptions;
-import com.google.gwtorm.server.OrmException;
 
 public interface Paginated<T> {
   QueryOptions getOptions();
 
-  ResultSet<T> restart(int start) throws OrmException;
+  ResultSet<T> restart(int start);
 }
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index 53c92c9..b5ed82d 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.Iterables;
@@ -82,6 +83,8 @@
 
   /** Invert the passed node. */
   public static <T> Predicate<T> not(Predicate<T> that) {
+    checkArgument(
+        !(that instanceof Any), "negating any() is unsafe because it post-filters all results");
     if (that instanceof NotPredicate) {
       // Negate of a negate is the original predicate.
       //
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
index 12d1dd6..d24cfeb 100644
--- a/java/com/google/gerrit/index/query/QueryBuilder.java
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.index.query;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.index.query.Predicate.and;
 import static com.google.gerrit.index.query.Predicate.not;
 import static com.google.gerrit.index.query.Predicate.or;
 import static com.google.gerrit.index.query.QueryParser.AND;
+import static com.google.gerrit.index.query.QueryParser.COLON;
 import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
 import static com.google.gerrit.index.query.QueryParser.EXACT_PHRASE;
 import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
@@ -25,7 +27,13 @@
 import static com.google.gerrit.index.query.QueryParser.OR;
 import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
 
+import com.google.common.base.Ascii;
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -34,9 +42,7 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import org.antlr.runtime.tree.Tree;
 
 /**
@@ -68,45 +74,60 @@
  * <p>Subclasses may also declare a handler for values which appear without operator by overriding
  * {@link #defaultField(String)}.
  *
+ * <p>Instances are non-singletons and should only be used once, in order to rescan the {@code
+ * DynamicMap} of plugin-provided operators on each query invocation.
+ *
  * @param <T> type of object the predicates can evaluate in memory.
  */
-public abstract class QueryBuilder<T> {
+public abstract class QueryBuilder<T, Q extends QueryBuilder<T, Q>> {
   /** Converts a value string passed to an operator into a {@link Predicate}. */
-  public interface OperatorFactory<T, Q extends QueryBuilder<T>> {
+  public interface OperatorFactory<T, Q extends QueryBuilder<T, Q>> {
     Predicate<T> create(Q builder, String value) throws QueryParseException;
   }
 
   /**
    * Defines the operators known by a QueryBuilder.
    *
-   * <p>This class is thread-safe and may be reused or cached.
+   * <p>Operators are discovered by scanning for methods annotated with {@link Operator}. Operator
+   * methods must be public, non-abstract, return a {@code Predicate}, and take a single string as
+   * an argument.
    *
-   * @param <T> type of object the predicates can evaluate in memory.
+   * <p>This class is deeply immutable.
+   *
+   * @param <T> type of object the predicates can evaluate.
    * @param <Q> type of the query builder subclass.
    */
-  public static class Definition<T, Q extends QueryBuilder<T>> {
-    private final Map<String, OperatorFactory<T, Q>> opFactories = new HashMap<>();
+  public static class Definition<T, Q extends QueryBuilder<T, Q>> {
+    private final ImmutableMap<String, OperatorFactory<T, Q>> opFactories;
 
-    public Definition(Class<Q> clazz) {
-      // Guess at the supported operators by scanning methods.
-      //
+    public Definition(Class<? extends Q> clazz) {
+      ImmutableMap.Builder<String, OperatorFactory<T, Q>> b = ImmutableMap.builder();
       Class<?> c = clazz;
       while (c != QueryBuilder.class) {
         for (Method method : c.getDeclaredMethods()) {
-          if (method.getAnnotation(Operator.class) != null
-              && Predicate.class.isAssignableFrom(method.getReturnType())
-              && method.getParameterTypes().length == 1
-              && method.getParameterTypes()[0] == String.class
-              && (method.getModifiers() & Modifier.ABSTRACT) == 0
-              && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) {
-            final String name = method.getName().toLowerCase();
-            if (!opFactories.containsKey(name)) {
-              opFactories.put(name, new ReflectionFactory<>(name, method));
-            }
+          if (method.getAnnotation(Operator.class) == null) {
+            continue;
           }
+          checkArgument(
+              CharMatcher.ascii().matchesAllOf(method.getName()),
+              "method name must be ASCII: %s",
+              method.getName());
+          checkArgument(
+              Predicate.class.isAssignableFrom(method.getReturnType())
+                  && method.getParameterTypes().length == 1
+                  && method.getParameterTypes()[0] == String.class
+                  && Modifier.isPublic(method.getModifiers())
+                  && !Modifier.isAbstract(method.getModifiers()),
+              "method must be of the form \"@%s public Predicate<T> %s(String value)\": %s",
+              Operator.class.getSimpleName(),
+              method.getName(),
+              method);
+          String name = Ascii.toLowerCase(method.getName());
+          b.put(name, new ReflectionFactory<>(name, method));
         }
         c = c.getSuperclass();
       }
+      opFactories = b.build();
     }
   }
 
@@ -161,14 +182,26 @@
     return null;
   }
 
-  protected final Definition<T, ? extends QueryBuilder<T>> builderDef;
+  protected final Definition<T, Q> builderDef;
+  private final ImmutableMap<String, OperatorFactory<T, Q>> opFactories;
 
-  protected final Map<String, OperatorFactory<?, ?>> opFactories;
-
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
+  protected QueryBuilder(
+      Definition<T, Q> def,
+      @Nullable DynamicMap<? extends OperatorFactory<T, Q>> dynamicOpFactories) {
     builderDef = def;
-    opFactories = (Map) def.opFactories;
+
+    if (dynamicOpFactories != null) {
+      ImmutableMap.Builder<String, OperatorFactory<T, Q>> opFactoriesBuilder =
+          ImmutableMap.builder();
+      opFactoriesBuilder.putAll(def.opFactories);
+      for (Extension<? extends OperatorFactory<T, Q>> e : dynamicOpFactories) {
+        String name = e.getExportName() + "_" + e.getPluginName();
+        opFactoriesBuilder.put(name, e.getProvider().get());
+      }
+      opFactories = opFactoriesBuilder.build();
+    } else {
+      opFactories = def.opFactories;
+    }
   }
 
   /**
@@ -214,44 +247,44 @@
         return not(toPredicate(onlyChildOf(r)));
 
       case DEFAULT_FIELD:
-        return defaultField(onlyChildOf(r));
+        return defaultField(concatenateChildText(r));
 
       case FIELD_NAME:
-        return operator(r.getText(), onlyChildOf(r));
+        return operator(r.getText(), concatenateChildText(r));
 
       default:
         throw error("Unsupported operator: " + r);
     }
   }
 
-  private Predicate<T> operator(String name, Tree val) throws QueryParseException {
-    switch (val.getType()) {
-        // Expand multiple values, "foo:(a b c)", as though they were written
-        // out with the longer form, "foo:a foo:b foo:c".
-        //
-      case AND:
-      case OR:
-        {
-          List<Predicate<T>> p = new ArrayList<>(val.getChildCount());
-          for (int i = 0; i < val.getChildCount(); i++) {
-            final Tree c = val.getChild(i);
-            if (c.getType() != DEFAULT_FIELD) {
-              throw error("Nested operator not expected: " + c);
-            }
-            p.add(operator(name, onlyChildOf(c)));
-          }
-          return val.getType() == AND ? and(p) : or(p);
-        }
+  private static String concatenateChildText(Tree r) throws QueryParseException {
+    if (r.getChildCount() == 0) {
+      throw error("Expected children under: " + r);
+    }
+    if (r.getChildCount() == 1) {
+      return getFieldValue(r.getChild(0));
+    }
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < r.getChildCount(); i++) {
+      sb.append(getFieldValue(r.getChild(i)));
+    }
+    return sb.toString();
+  }
 
+  private static String getFieldValue(Tree r) throws QueryParseException {
+    if (r.getChildCount() != 0) {
+      throw error("Expected no children under: " + r);
+    }
+    switch (r.getType()) {
       case SINGLE_WORD:
+      case COLON:
       case EXACT_PHRASE:
-        if (val.getChildCount() != 0) {
-          throw error("Expected no children under: " + val);
-        }
-        return operator(name, val.getText());
-
+        return r.getText();
       default:
-        throw error("Unsupported node in operator " + name + ": " + val);
+        throw error(
+            String.format(
+                "Unsupported %s node in operator %s: %s",
+                QueryParser.tokenNames[r.getType()], r.getParent(), r));
     }
   }
 
@@ -265,20 +298,6 @@
     return f.create(this, value);
   }
 
-  private Predicate<T> defaultField(Tree r) throws QueryParseException {
-    switch (r.getType()) {
-      case SINGLE_WORD:
-      case EXACT_PHRASE:
-        if (r.getChildCount() != 0) {
-          throw error("Expected no children under: " + r);
-        }
-        return defaultField(r.getText());
-
-      default:
-        throw error("Unsupported node: " + r);
-    }
-  }
-
   /**
    * Handle a value present outside of an operator.
    *
@@ -322,7 +341,7 @@
   @Target(ElementType.METHOD)
   protected @interface Operator {}
 
-  private static class ReflectionFactory<T, Q extends QueryBuilder<T>>
+  private static class ReflectionFactory<T, Q extends QueryBuilder<T, Q>>
       implements OperatorFactory<T, Q> {
     private final String name;
     private final Method method;
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index c3e1f63..7077245 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexConfig;
@@ -37,8 +38,6 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.logging.CallerFinder;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -89,6 +88,7 @@
 
   private boolean enforceVisibility = true;
   private int userProvidedLimit;
+  private boolean isNoLimit;
   private Set<String> requestedFields;
 
   protected QueryProcessor(
@@ -155,6 +155,11 @@
     return this;
   }
 
+  public QueryProcessor<T> setNoLimit(boolean isNoLimit) {
+    this.isNoLimit = isNoLimit;
+    return this;
+  }
+
   public QueryProcessor<T> setRequestedFields(Set<String> fields) {
     requestedFields = fields;
     return this;
@@ -167,7 +172,7 @@
    * @param query the query.
    * @return results of the query.
    */
-  public QueryResult<T> query(Predicate<T> query) throws OrmException, QueryParseException {
+  public QueryResult<T> query(Predicate<T> query) throws QueryParseException {
     return query(ImmutableList.of(query)).get(0);
   }
 
@@ -182,13 +187,10 @@
    * @return results of the queries, one QueryResult per input query, in the same order as the
    *     input.
    */
-  public List<QueryResult<T>> query(List<Predicate<T>> queries)
-      throws OrmException, QueryParseException {
+  public List<QueryResult<T>> query(List<Predicate<T>> queries) throws QueryParseException {
     try {
       return query(null, queries);
-    } catch (OrmRuntimeException e) {
-      throw new OrmException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       if (e.getCause() != null) {
         Throwables.throwIfInstanceOf(e.getCause(), QueryParseException.class);
       }
@@ -197,8 +199,7 @@
   }
 
   private List<QueryResult<T>> query(
-      @Nullable List<String> queryStrings, List<Predicate<T>> queries)
-      throws OrmException, QueryParseException {
+      @Nullable List<String> queryStrings, List<Predicate<T>> queries) throws QueryParseException {
     long startNanos = System.nanoTime();
     checkState(!used.getAndSet(true), "%s has already been used", getClass().getSimpleName());
     int cnt = queries.size();
@@ -281,7 +282,7 @@
       // Only measure successful queries that actually touched the index.
       metrics.executionTime.record(
           schemaDef.getName(), System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
-    } catch (OrmException | OrmRuntimeException e) {
+    } catch (StorageException e) {
       Optional<QueryParseException> qpe = findQueryParseException(e);
       if (qpe.isPresent()) {
         throw new QueryParseException(qpe.get().getMessage(), e);
@@ -352,6 +353,9 @@
   }
 
   private int getEffectiveLimit(Predicate<T> p) {
+    if (isNoLimit == true) {
+      return Integer.MAX_VALUE;
+    }
     List<Integer> possibleLimits = new ArrayList<>(4);
     possibleLimits.add(getBackendSupportedLimit());
     possibleLimits.add(getPermittedLimit());
@@ -371,8 +375,7 @@
   }
 
   private static Optional<QueryParseException> findQueryParseException(Throwable t) {
-    return Throwables.getCausalChain(t)
-        .stream()
+    return Throwables.getCausalChain(t).stream()
         .filter(c -> c instanceof QueryParseException)
         .map(QueryParseException.class::cast)
         .findFirst();
diff --git a/java/com/google/gerrit/index/query/testing/BUILD b/java/com/google/gerrit/index/query/testing/BUILD
new file mode 100644
index 0000000..ee346a8
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/BUILD
@@ -0,0 +1,15 @@
+package(
+    default_testonly = True,
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "testing",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//antlr3:query_parser",
+        "//lib:guava",
+        "//lib/antlr:java-runtime",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/index/query/testing/TreeSubject.java b/java/com/google/gerrit/index/query/testing/TreeSubject.java
new file mode 100644
index 0000000..c60b363
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/TreeSubject.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index.query.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.index.query.QueryParser;
+import org.antlr.runtime.tree.Tree;
+
+public class TreeSubject extends Subject<TreeSubject, Tree> {
+  public static TreeSubject assertThat(Tree actual) {
+    return assertAbout(TreeSubject::new).that(actual);
+  }
+
+  private TreeSubject(FailureMetadata failureMetadata, Tree tree) {
+    super(failureMetadata, tree);
+  }
+
+  public void hasType(int expectedType) {
+    isNotNull();
+    check("getType()").that(typeName(actual().getType())).isEqualTo(typeName(expectedType));
+  }
+
+  public void hasText(String expectedText) {
+    requireNonNull(expectedText);
+    isNotNull();
+    check("getText()").that(actual().getText()).isEqualTo(expectedText);
+  }
+
+  public void hasNoChildren() {
+    isNotNull();
+    check("getChildCount()").that(actual().getChildCount()).isEqualTo(0);
+  }
+
+  public void hasChildCount(int expectedChildCount) {
+    checkArgument(
+        expectedChildCount > 0, "expected child count must be positive: %s", expectedChildCount);
+    isNotNull();
+    check("getChildCount()").that(actual().getChildCount()).isEqualTo(expectedChildCount);
+  }
+
+  public TreeSubject child(int childIndex) {
+    isNotNull();
+    return check("getChild(%s)", childIndex)
+        .about(TreeSubject::new)
+        .that(actual().getChild(childIndex));
+  }
+
+  private static String typeName(int type) {
+    checkArgument(
+        type >= 0 && type < QueryParser.tokenNames.length,
+        "invalid token type %s, max is %s",
+        type,
+        QueryParser.tokenNames.length - 1);
+    return QueryParser.tokenNames[type];
+  }
+}
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 30d4e15..dec077a 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -625,9 +625,7 @@
    * @return true if any thread has a stack frame in {@code org.eclipse.jdt}.
    */
   public static boolean isRunningInEclipse() {
-    return Thread.getAllStackTraces()
-        .values()
-        .stream()
+    return Thread.getAllStackTraces().values().stream()
         .flatMap(Arrays::stream)
         .anyMatch(e -> e.getClassName().startsWith("org.eclipse.jdt."));
   }
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 d9ca76d..7a0430c 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -30,6 +30,8 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
@@ -44,7 +46,6 @@
 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 java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Map;
@@ -212,7 +213,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     IndexUtils.setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
@@ -286,8 +287,12 @@
   }
 
   @Override
-  public void deleteAll() throws IOException {
-    writer.deleteAll();
+  public void deleteAll() {
+    try {
+      writer.deleteAll();
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
   }
 
   public IndexWriter getWriter() {
@@ -312,6 +317,14 @@
     return result;
   }
 
+  /**
+   * Trasform an index document into a target object type.
+   *
+   * @param doc index document
+   * @return target object, or null if the target object was not found or failed to load from the
+   *     underlying store.
+   */
+  @Nullable
   protected abstract V fromDocument(Document doc);
 
   void add(Document doc, Values<V> values) {
@@ -477,16 +490,16 @@
     }
 
     @Override
-    public ResultSet<V> read() throws OrmException {
+    public ResultSet<V> read() {
       return readImpl(AbstractLuceneIndex.this::fromDocument);
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       return readImpl(AbstractLuceneIndex.this::toFieldBundle);
     }
 
-    private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
+    private <T> ResultSet<T> readImpl(Function<Document, T> mapper) {
       IndexSearcher searcher = null;
       try {
         searcher = acquire();
@@ -503,7 +516,7 @@
         }
         return new ListResultSet<>(b.build());
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       } finally {
         if (searcher != null) {
           try {
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 3b18f2c..fa4c923 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -10,7 +10,6 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/lucene:lucene-core-and-backward-codecs",
     ],
 )
@@ -26,6 +25,7 @@
         ":query_builder",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
@@ -36,7 +36,6 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/flogger:api",
         "//lib/guice",
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 7d7cbef..98424b5 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -71,12 +71,12 @@
   }
 
   @Override
-  public void replace(ChangeData obj) throws IOException {
+  public void replace(ChangeData obj) {
     throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
   }
 
   @Override
-  public void delete(Change.Id key) throws IOException {
+  public void delete(Change.Id key) {
     throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 86a2111..0b787b6 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.index.account.AccountField.ID;
 import static com.google.gerrit.server.index.account.AccountField.PREFERRED_EMAIL_EXACT;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -120,20 +121,20 @@
   }
 
   @Override
-  public void replace(AccountState as) throws IOException {
+  public void replace(AccountState as) {
     try {
       replace(idTerm(as), toDocument(as)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Account.Id key) throws IOException {
+  public void delete(Account.Id key) {
     try {
       delete(idTerm(key)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index a3394c3..8bf0b6b 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -36,6 +36,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
@@ -62,8 +63,6 @@
 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.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.protobuf.MessageLite;
@@ -201,34 +200,34 @@
   }
 
   @Override
-  public void replace(ChangeData cd) throws IOException {
+  public void replace(ChangeData cd) {
     Term id = LuceneChangeIndex.idTerm(cd);
     // toDocument is essentially static and doesn't depend on the specific
     // sub-index, so just pick one.
     Document doc = openIndex.toDocument(cd);
     try {
-      if (cd.change().getStatus().isOpen()) {
+      if (cd.change().isNew()) {
         Futures.allAsList(closedIndex.delete(id), openIndex.replace(id, doc)).get();
       } else {
         Futures.allAsList(openIndex.delete(id), closedIndex.replace(id, doc)).get();
       }
-    } catch (OrmException | ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+    } catch (ExecutionException | InterruptedException e) {
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Change.Id id) throws IOException {
+  public void delete(Change.Id id) {
     Term idTerm = LuceneChangeIndex.idTerm(id);
     try {
       Futures.allAsList(openIndex.delete(idTerm), closedIndex.delete(idTerm)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     openIndex.deleteAll();
     closedIndex.deleteAll();
   }
@@ -248,7 +247,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     // Arbitrary done on open index, as ready bit is set
     // per index and not sub index
     openIndex.markReady(ready);
@@ -303,10 +302,10 @@
     }
 
     @Override
-    public ResultSet<ChangeData> read() throws OrmException {
+    public ResultSet<ChangeData> read() {
       if (Thread.interrupted()) {
         Thread.currentThread().interrupt();
-        throw new OrmException("interrupted");
+        throw new StorageException("interrupted");
       }
 
       final Set<String> fields = IndexUtils.changeFields(opts);
@@ -327,12 +326,12 @@
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       List<Document> documents;
       try {
         documents = doRead(IndexUtils.changeFields(opts));
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
       ImmutableList<FieldBundle> fieldBundles =
           documents.stream().map(rawDocumentMapper).collect(toImmutableList());
@@ -415,10 +414,10 @@
         return result.build();
       } catch (InterruptedException e) {
         close();
-        throw new OrmRuntimeException(e);
+        throw new StorageException(e);
       } catch (ExecutionException e) {
         Throwables.throwIfUnchecked(e.getCause());
-        throw new OrmRuntimeException(e.getCause());
+        throw new StorageException(e.getCause());
       }
     }
 
@@ -652,8 +651,7 @@
 
   private static <T> List<T> decodeProtos(
       ListMultimap<String, IndexableField> doc, String fieldName, ProtoConverter<?, T> converter) {
-    return doc.get(fieldName)
-        .stream()
+    return doc.get(fieldName).stream()
         .map(IndexableField::binaryValue)
         .map(bytesRef -> parseProtoFrom(bytesRef, converter))
         .collect(toImmutableList());
@@ -668,8 +666,7 @@
   }
 
   private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
-    return fields
-        .stream()
+    return fields.stream()
         .map(
             f -> {
               BytesRef ref = f.binaryValue();
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 95e2ab9..0fdef77 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.server.index.group.GroupField.UUID;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -110,20 +111,20 @@
   }
 
   @Override
-  public void replace(InternalGroup group) throws IOException {
+  public void replace(InternalGroup group) {
     try {
       replace(idTerm(group), toDocument(group)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(AccountGroup.UUID key) throws IOException {
+  public void delete(AccountGroup.UUID key) {
     try {
       delete(idTerm(key)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 807c40a..950e206 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.index.project.ProjectField.NAME;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
@@ -31,6 +32,7 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -109,20 +111,20 @@
   }
 
   @Override
-  public void replace(ProjectData projectState) throws IOException {
+  public void replace(ProjectData projectState) {
     try {
       replace(idTerm(projectState), toDocument(projectState)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
   @Override
-  public void delete(Project.NameKey nameKey) throws IOException {
+  public void delete(Project.NameKey nameKey) {
     try {
       delete(idTerm(nameKey)).get();
     } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -138,6 +140,7 @@
   @Override
   protected ProjectData fromDocument(Document doc) {
     Project.NameKey nameKey = new Project.NameKey(doc.getField(NAME.getName()).stringValue());
-    return projectCache.get().get(nameKey).toProjectData();
+    ProjectState projectState = projectCache.get().get(nameKey);
+    return projectState == null ? null : projectState.toProjectData();
   }
 }
diff --git a/java/com/google/gerrit/mail/HtmlParser.java b/java/com/google/gerrit/mail/HtmlParser.java
index 9821599..265c412 100644
--- a/java/com/google/gerrit/mail/HtmlParser.java
+++ b/java/com/google/gerrit/mail/HtmlParser.java
@@ -78,8 +78,7 @@
     for (Element e : d.body().getAllElements()) {
       String elementName = e.tagName();
       boolean isInBlockQuote =
-          e.parents()
-              .stream()
+          e.parents().stream()
               .anyMatch(
                   p ->
                       p.tagName().equals("blockquote")
diff --git a/java/com/google/gerrit/mail/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
index 00754d3..b7e2030 100644
--- a/java/com/google/gerrit/mail/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -85,10 +85,7 @@
     }
 
     // Add additional headers
-    mimeMessage
-        .getHeader()
-        .getFields()
-        .stream()
+    mimeMessage.getHeader().getFields().stream()
         .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase()))
         .forEach(f -> messageBuilder.addAdditionalHeader(f.getName() + ": " + f.getBody()));
 
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 0bebad4..02c083c 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -18,6 +18,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/httpd",
@@ -49,7 +50,6 @@
         "//java/com/google/gerrit/util/http",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib:servlet-api-3_1-without-neverlink",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 3c4f89b..e2fd7f3 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.gerrit.common.Version.getVersion;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -73,8 +75,8 @@
 import com.google.gerrit.server.config.SysExecutorModule;
 import com.google.gerrit.server.events.EventBroker;
 import com.google.gerrit.server.events.StreamEventsApiListener;
-import com.google.gerrit.server.git.ChangeRefCache;
 import com.google.gerrit.server.git.GarbageCollectionModule;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.group.PeriodicGroupIndexer;
 import com.google.gerrit.server.index.IndexModule;
@@ -118,6 +120,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import org.eclipse.jgit.lib.Config;
@@ -182,7 +185,7 @@
   private boolean inMemoryTest;
   private AbstractModule luceneModule;
   private Module emailModule;
-  private Module testSysModule;
+  private List<Module> testSysModules = new ArrayList<>();
   private Module auditEventModule;
 
   private Runnable serverStarted;
@@ -309,8 +312,8 @@
   }
 
   @VisibleForTesting
-  public void setAdditionalSysModuleForTesting(@Nullable Module m) {
-    testSysModule = m;
+  public void addAdditionalSysModuleForTesting(@Nullable Module... modules) {
+    testSysModules.addAll(Arrays.asList(modules));
   }
 
   @VisibleForTesting
@@ -363,7 +366,15 @@
   }
 
   private String myVersion() {
-    return com.google.gerrit.common.Version.getVersion();
+    List<String> versionParts = new ArrayList<>();
+    if (slave) {
+      versionParts.add("[slave]");
+    }
+    if (headless) {
+      versionParts.add("[headless]");
+    }
+    versionParts.add(getVersion());
+    return Joiner.on(" ").join(versionParts);
   }
 
   private Injector createCfgInjector() {
@@ -393,7 +404,7 @@
     modules.add(new GerritApiModule());
     modules.add(new PluginApiModule());
 
-    modules.add(new ChangeRefCache.Module());
+    modules.add(new SearchingChangeCacheImpl.Module(slave));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultMemoryCacheModule());
@@ -461,9 +472,7 @@
       modules.add(new AccountDeactivator.Module());
       modules.add(new ChangeCleanupRunner.Module());
     }
-    if (testSysModule != null) {
-      modules.add(testSysModule);
-    }
+    modules.addAll(testSysModules);
     modules.add(new LocalMergeSuperSetComputation.Module());
     modules.add(new DefaultProjectNameLockManager.Module());
     return cfgInjector.createChildInjector(
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 14a0b5d..e6e091c 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
@@ -96,7 +96,7 @@
   }
 
   private void convertLocalUserToLowerCase(ExternalIdNotes extIdNotes, ExternalId extId)
-      throws OrmDuplicateKeyException, IOException {
+      throws DuplicateKeyException, IOException {
     if (extId.isScheme(SCHEME_GERRIT)) {
       String localUser = extId.key().id();
       String localUserLowerCase = localUser.toLowerCase(Locale.US);
diff --git a/java/com/google/gerrit/pgm/PrologShell.java b/java/com/google/gerrit/pgm/PrologShell.java
index 5decd68..2780f84 100644
--- a/java/com/google/gerrit/pgm/PrologShell.java
+++ b/java/com/google/gerrit/pgm/PrologShell.java
@@ -30,9 +30,14 @@
   @Option(name = "-s", metaVar = "FILE.pl", usage = "file to load")
   private List<String> fileName = new ArrayList<>();
 
+  @Option(name = "-q", usage = "quiet mode without banner")
+  private boolean quiet;
+
   @Override
   public int run() {
-    banner();
+    if (!quiet) {
+      banner();
+    }
 
     BufferingPrologControl pcl = new BufferingPrologControl();
     pcl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
@@ -55,7 +60,9 @@
 
     try {
       pcl.execute(Prolog.BUILTIN, "cafeteria");
-      write("% halt\n");
+      if (!quiet) {
+        write("% halt\n");
+      }
       return 0;
     } catch (HaltException halt) {
       write("% halt(" + halt.getStatus() + ")\n");
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index cbc9c3b..0e5f659 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -38,7 +38,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -114,7 +113,7 @@
     return true;
   }
 
-  private boolean reindex() throws IOException {
+  private boolean reindex() {
     boolean ok = true;
     for (IndexDefinition<?, ?, ?> def : indexDefs) {
       if (indices.isEmpty() || indices.contains(def.getName())) {
@@ -186,8 +185,7 @@
     globalConfig.setBoolean("index", null, "autoReindexIfStale", false);
   }
 
-  private <K, V, I extends Index<K, V>> boolean reindex(IndexDefinition<K, V, I> def)
-      throws IOException {
+  private <K, V, I extends Index<K, V>> boolean reindex(IndexDefinition<K, V, I> def) {
     I index = def.getIndexCollection().getSearchIndex();
     requireNonNull(
         index, () -> String.format("no active search index configured for %s", def.getName()));
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index 3c6df14..b2a4d72 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/launcher",
@@ -21,7 +22,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib/commons:validator",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index c1fd1df..9c158b7 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -44,7 +44,6 @@
 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.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -350,7 +349,7 @@
       this.repositoryManager = repositoryManager;
     }
 
-    void upgradeSchema() throws OrmException {
+    void upgradeSchema() {
       noteDbSchemaUpdater.update(new UpdateUIImpl(ui));
     }
 
diff --git a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 6336c93..9519653 100644
--- a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
@@ -49,7 +48,7 @@
   }
 
   public synchronized void insert(String commitMessage, Collection<ExternalId> extIds)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     File path = getPath();
     if (path != null) {
       try (Repository allUsersRepo = new FileRepository(path)) {
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 584d8af..273ebfb 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -20,7 +20,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.reviewdb.client.Account;
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index f19cf39..27e6ce9 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -18,7 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
diff --git a/java/com/google/gerrit/pgm/init/InitLogging.java b/java/com/google/gerrit/pgm/init/InitLogging.java
index 52d0d2f..b6d25bc 100644
--- a/java/com/google/gerrit/pgm/init/InitLogging.java
+++ b/java/com/google/gerrit/pgm/init/InitLogging.java
@@ -53,8 +53,7 @@
   }
 
   private static boolean isSet(List<String> javaOptions, String javaOptionName) {
-    return javaOptions
-        .stream()
+    return javaOptions.stream()
         .anyMatch(
             o ->
                 o.startsWith("-D" + javaOptionName + "=")
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index bc418dd..5b07fc6 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -5,10 +5,10 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 444f64f..ea39a44 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -224,7 +224,7 @@
 
     @Override
     public void header(String fmt, Object... args) {
-      fmt = fmt.replaceAll("\n", "\n*** ");
+      fmt = fmt.replace("\n", "\n*** ");
       console.printf("\n*** " + fmt + "\n*** \n\n", args);
     }
 
diff --git a/java/com/google/gerrit/pgm/init/api/InitUtil.java b/java/com/google/gerrit/pgm/init/api/InitUtil.java
index 656f53a..d038de7 100644
--- a/java/com/google/gerrit/pgm/init/api/InitUtil.java
+++ b/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -97,7 +97,7 @@
       p = name.indexOf(".");
       if (0 < p) {
         name = name.substring(p + 1);
-        name = "DC=" + name.replaceAll("\\.", ",DC=");
+        name = "DC=" + name.replace(".", ",DC=");
       } else {
         name = null;
       }
diff --git a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
index 70f0bb6..d3d22cb 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.server.Sequences;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.RepoSequence;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -34,7 +33,7 @@
     this.allUsersName = allUsersName;
   }
 
-  public int nextAccountId() throws OrmException {
+  public int nextAccountId() {
     RepoSequence accountSeq =
         new RepoSequence(
             repoManager,
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..ffd1cbd 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -19,8 +19,6 @@
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
-        "//lib/commons:dbcp",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index f263786..956ec75 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
@@ -48,16 +49,17 @@
 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.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;
 import com.google.gerrit.server.extensions.events.EventUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
-import com.google.gerrit.server.git.ChangeRefCache;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.NoteDbModule;
@@ -72,7 +74,6 @@
 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;
@@ -112,16 +113,16 @@
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
         .in(SINGLETON);
-    bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {})
-        .toInstance(DynamicMap.emptyMap());
+    bind(new TypeLiteral<DynamicSet<ChangeAttributeFactory>>() {})
+        .toInstance(DynamicSet.emptySet());
     bind(new TypeLiteral<DynamicMap<RestView<CommitResource>>>() {})
         .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.of(null));
@@ -133,7 +134,7 @@
 
     // As Reindex is a batch program, don't assume the index is available for
     // the change cache.
-    bind(ChangeRefCache.class).toProvider(Providers.of(null));
+    bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
 
     bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
         .annotatedWith(AdministrateServerGroups.class)
@@ -160,6 +161,7 @@
     install(ChangeKindCacheImpl.module());
     install(MergeabilityCacheImpl.module());
     install(TagCache.module());
+    install(PureRevertCache.module());
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ProjectState.Factory.class);
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index 8a9ebdb..7c384e7 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
 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.CreationException;
 import com.google.inject.Guice;
@@ -38,20 +37,17 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 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"},
       usage = "Local directory containing site data")
   private void setSitePath(String path) {
-    sitePath = Paths.get(path);
+    sitePath = Paths.get(path).normalize();
   }
 
   private Path sitePath = Paths.get(".");
@@ -59,7 +55,7 @@
   protected SiteProgram() {}
 
   protected SiteProgram(Path sitePath) {
-    this.sitePath = sitePath;
+    this.sitePath = sitePath.normalize();
   }
 
   /** @return the site path specified on the command line. */
@@ -127,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());
@@ -164,11 +147,4 @@
   protected final String getConfiguredSecureStoreClass() {
     return getSecureStoreClassName(sitePath);
   }
-
-  @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/proto/testing/SerializedClassSubject.java b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
index 9ca6c9b..546ff89 100644
--- a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -15,9 +15,8 @@
 package com.google.gerrit.proto.testing;
 
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
-import 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;
@@ -67,23 +66,23 @@
 
   public void isAbstract() {
     isNotNull();
-    assertWithMessage("expected class %s to be abstract", actual().getName())
-        .that(Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
+    if (!Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be abstract"));
+    }
   }
 
   public void isConcrete() {
     isNotNull();
-    assertWithMessage("expected class %s to be concrete", actual().getName())
-        .that(!Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
+    if (Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be concrete"));
+    }
   }
 
   public void hasFields(Map<String, Type> expectedFields) {
     isConcrete();
-    assertThat(
-            FieldUtils.getAllFieldsList(actual())
-                .stream()
+    check("fields()")
+        .that(
+            FieldUtils.getAllFieldsList(actual()).stream()
                 .filter(f -> !Modifier.isStatic(f.getModifiers()))
                 .collect(toImmutableMap(Field::getName, Field::getGenericType)))
         .containsExactlyEntriesIn(expectedFields);
@@ -92,20 +91,20 @@
   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(
+    check("noArgumentAbstractMethodsOn(%s)", actual().getName())
+        .that(
             Arrays.stream(actual().getDeclaredMethods())
                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
                 .filter(m -> Modifier.isAbstract(m.getModifiers()))
                 .filter(m -> m.getParameters().length == 0)
                 .collect(toImmutableMap(Method::getName, Method::getGenericReturnType)))
-        .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())
+    check("superclass(%s)", actual().getName())
+        .that(actual().getGenericSuperclass())
         .isEqualTo(superclassType);
   }
 }
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index 74c3403..588c47a 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -8,8 +8,8 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//proto:entities_java_proto",
     ],
diff --git a/java/com/google/gerrit/reviewdb/client/Account.java b/java/com/google/gerrit/reviewdb/client/Account.java
index 95da391..47c9b40 100644
--- a/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/java/com/google/gerrit/reviewdb/client/Account.java
@@ -18,6 +18,7 @@
 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.IntKey;
 import java.sql.Timestamp;
@@ -69,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) {
@@ -150,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/Change.java b/java/com/google/gerrit/reviewdb/client/Change.java
index a8a3304..c1443f2 100644
--- a/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/java/com/google/gerrit/reviewdb/client/Change.java
@@ -671,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;
   }
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index 0200c28..fd427f7 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -30,7 +30,13 @@
   /** Default submit type for root project (All-Projects). */
   public static final SubmitType DEFAULT_ALL_PROJECTS_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
 
-  /** Project name key */
+  /**
+   * Project name key.
+   *
+   * <p>This class has subclasses such as {@code AllProjectsName}, which make Guice injection more
+   * convenient. Subclasses must compare equal if they have the same name, regardless of the
+   * specific class. This implies that subclasses may not add additional fields.
+   */
   public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
@@ -53,12 +59,12 @@
     }
 
     @Override
-    public int hashCode() {
+    public final int hashCode() {
       return get().hashCode();
     }
 
     @Override
-    public boolean equals(Object b) {
+    public final boolean equals(Object b) {
       if (b instanceof NameKey) {
         return get().equals(((NameKey) b).get());
       }
@@ -95,8 +101,6 @@
 
   protected String localDefaultDashboardId;
 
-  protected String themeName;
-
   protected String configRefState;
 
   protected Project() {}
@@ -182,22 +186,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 9e1fcca..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";
@@ -137,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());
   }
@@ -193,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");
     }
@@ -266,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;
@@ -308,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;
     }
diff --git a/java/com/google/gerrit/reviewdb/client/TrackingId.java b/java/com/google/gerrit/reviewdb/client/TrackingId.java
deleted file mode 100644
index 57d786b..0000000
--- a/java/com/google/gerrit/reviewdb/client/TrackingId.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.CompoundKey;
-import com.google.gwtorm.client.StringKey;
-
-/** External tracking id associated with a {@link Change} */
-public final class TrackingId {
-  public static final int TRACKING_ID_MAX_CHAR = 32;
-  public static final int TRACKING_SYSTEM_MAX_CHAR = 10;
-
-  /** External tracking id */
-  public static class Id extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    protected String id;
-
-    protected Id() {}
-
-    public Id(String id) {
-      this.id = id;
-    }
-
-    @Override
-    public String get() {
-      return id;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      id = newValue;
-    }
-  }
-
-  /** Name of external tracking system */
-  public static class System extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    protected String system;
-
-    protected System() {}
-
-    public System(String s) {
-      this.system = s;
-    }
-
-    @Override
-    public String get() {
-      return system;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      system = newValue;
-    }
-  }
-
-  public static class Key extends CompoundKey<Change.Id> {
-    private static final long serialVersionUID = 1L;
-
-    protected Change.Id changeId;
-
-    protected Id trackingKey;
-
-    protected System trackingSystem;
-
-    protected Key() {
-      changeId = new Change.Id();
-      trackingKey = new Id();
-      trackingSystem = new System();
-    }
-
-    protected Key(Change.Id ch, Id id, System s) {
-      changeId = ch;
-      trackingKey = id;
-      trackingSystem = s;
-    }
-
-    @Override
-    public Change.Id getParentKey() {
-      return changeId;
-    }
-
-    public TrackingId.Id getTrackingId() {
-      return trackingKey;
-    }
-
-    public TrackingId.System getTrackingSystem() {
-      return trackingSystem;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {trackingKey, trackingSystem};
-    }
-  }
-
-  protected Key key;
-
-  protected TrackingId() {}
-
-  public TrackingId(Change.Id ch, TrackingId.Id id, TrackingId.System s) {
-    key = new Key(ch, id, s);
-  }
-
-  public TrackingId(Change.Id ch, String id, String s) {
-    key = new Key(ch, new TrackingId.Id(id), new TrackingId.System(s));
-  }
-
-  public TrackingId.Key getKey() {
-    return key;
-  }
-
-  public Change.Id getChangeId() {
-    return key.changeId;
-  }
-
-  public String getTrackingId() {
-    return key.trackingKey.get();
-  }
-
-  public String getSystem() {
-    return key.trackingSystem.get();
-  }
-
-  @Override
-  public int hashCode() {
-    return key.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (obj instanceof TrackingId) {
-      final TrackingId tr = (TrackingId) obj;
-      return key.equals(tr.key);
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/ApprovalCopier.java b/java/com/google/gerrit/server/ApprovalCopier.java
index 45588a3..a1df711 100644
--- a/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/ApprovalCopier.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -73,8 +73,7 @@
   }
 
   Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig)
-      throws OrmException {
+      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
     return getForPatchSet(notes, psId, rw, repoConfig, Collections.emptyList());
   }
 
@@ -83,8 +82,7 @@
       PatchSet.Id psId,
       @Nullable RevWalk rw,
       @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy)
-      throws OrmException {
+      Iterable<PatchSetApproval> dontCopy) {
     PatchSet ps = psUtil.get(notes, psId);
     if (ps == null) {
       return Collections.emptyList();
@@ -97,8 +95,7 @@
       PatchSet ps,
       @Nullable RevWalk rw,
       @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy)
-      throws OrmException {
+      Iterable<PatchSetApproval> dontCopy) {
     requireNonNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(notes);
     try {
@@ -153,11 +150,11 @@
       }
       return labelNormalizer.normalize(notes, byUser.values()).getNormalized();
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd) throws OrmException {
+  private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd) {
     Collection<PatchSet> patchSets = cd.patchSets();
     TreeMap<Integer, PatchSet> result = new TreeMap<>();
     for (PatchSet ps : patchSets) {
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 1b5983a4..865e33c 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -47,7 +48,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -112,9 +112,8 @@
    *
    * @param notes change notes.
    * @return reviewers for the change.
-   * @throws OrmException if reviewers for the change could not be read.
    */
-  public ReviewerSet getReviewers(ChangeNotes notes) throws OrmException {
+  public ReviewerSet getReviewers(ChangeNotes notes) {
     return notes.load().getReviewers();
   }
 
@@ -123,10 +122,8 @@
    *
    * @param allApprovals all approvals to consider; must all belong to the same change.
    * @return reviewers for the change.
-   * @throws OrmException if reviewers for the change could not be read.
    */
-  public ReviewerSet getReviewers(ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
-      throws OrmException {
+  public ReviewerSet getReviewers(ChangeNotes notes, Iterable<PatchSetApproval> allApprovals) {
     return notes.load().getReviewers();
   }
 
@@ -135,9 +132,8 @@
    *
    * @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 {
+  public List<ReviewerStatusUpdate> getReviewerUpdates(ChangeNotes notes) {
     return notes.load().getReviewerUpdates();
   }
 
@@ -165,8 +161,7 @@
       ChangeUpdate update,
       LabelTypes labelTypes,
       Change change,
-      Iterable<Account.Id> wantReviewers)
-      throws OrmException {
+      Iterable<Account.Id> wantReviewers) {
     PatchSet.Id psId = change.currentPatchSetId();
     Collection<Account.Id> existingReviewers;
     existingReviewers = notes.load().getReviewers().byState(REVIEWER);
@@ -245,10 +240,9 @@
    * @param update change update.
    * @param wantCCs accounts to CC.
    * @return whether a change was made.
-   * @throws OrmException
    */
   public Collection<Account.Id> addCcs(
-      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) throws OrmException {
+      ChangeNotes notes, ChangeUpdate update, Collection<Account.Id> wantCCs) {
     return addCcs(update, wantCCs, notes.load().getReviewers());
   }
 
@@ -272,7 +266,6 @@
    * @param user user adding approvals.
    * @param approvals approvals to add.
    * @throws RestApiException
-   * @throws OrmException
    */
   public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(
       ChangeUpdate update,
@@ -280,7 +273,7 @@
       PatchSet ps,
       CurrentUser user,
       Map<String, Short> approvals)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     Account.Id accountId = user.getAccountId();
     checkArgument(
         accountId.equals(ps.getUploader()),
@@ -330,14 +323,12 @@
     }
   }
 
-  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ChangeNotes notes)
-      throws OrmException {
+  public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ChangeNotes notes) {
     return notes.load().getApprovals();
   }
 
   public Iterable<PatchSetApproval> byPatchSet(
-      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig)
-      throws OrmException {
+      ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
     return copier.getForPatchSet(notes, psId, rw, repoConfig);
   }
 
@@ -346,8 +337,7 @@
       PatchSet.Id psId,
       Account.Id accountId,
       @Nullable RevWalk rw,
-      @Nullable Config repoConfig)
-      throws OrmException {
+      @Nullable Config repoConfig) {
     return filterApprovals(byPatchSet(notes, psId, rw, repoConfig), accountId);
   }
 
@@ -358,7 +348,7 @@
     try {
       // Submit approval is never copied, so bypass expensive byPatchSet call.
       return getSubmitter(c, byChange(notes).get(c));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index e5a5152..34e9f3a 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -1,3 +1,5 @@
+load("//tools/bzl:javadoc.bzl", "java_doc")
+
 CONSTANTS_SRC = [
     "documentation/Constants.java",
 ]
@@ -31,6 +33,7 @@
         ":constants",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
@@ -50,6 +53,7 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/ssl",
+        "//java/com/google/gwtorm",
         "//java/org/apache/commons/net",
         "//java/org/eclipse/jgit:server",
         "//lib:args4j",
@@ -84,7 +88,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
@@ -143,8 +146,6 @@
     ],
 )
 
-load("//tools/bzl:javadoc.bzl", "java_doc")
-
 java_doc(
     name = "doc",
     libs = [":server"],
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 8b9a6f4..97ba8f0 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
 import java.util.List;
@@ -87,7 +86,7 @@
     return workInProgress ? TAG_UPLOADED_WIP_PATCH_SET : TAG_UPLOADED_PATCH_SET;
   }
 
-  public List<ChangeMessage> byChange(ChangeNotes notes) throws OrmException {
+  public List<ChangeMessage> byChange(ChangeNotes notes) {
     return notes.load().getChangeMessages();
   }
 
@@ -109,12 +108,11 @@
    * rather than an ID allowed us to delete the message from both NoteDb and ReviewDb.
    *
    * @param update change update.
-   * @param targetMessageIdx the index of the target change message.
+   * @param targetMessageId the id of the target change message.
    * @param newMessage the new message which is going to replace the old.
    */
-  // 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);
+  public void replaceChangeMessage(ChangeUpdate update, String targetMessageId, String newMessage) {
+    update.deleteChangeMessageByRewritingHistory(targetMessageId, newMessage);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index 7a6d3e6..b8a00f4 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -116,9 +116,7 @@
    */
   public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) throws IOException {
     return nextPatchSetIdFromChangeRefs(
-        git.getRefDatabase()
-            .getRefsByPrefix(id.getParentKey().toRefPrefix())
-            .stream()
+        git.getRefDatabase().getRefsByPrefix(id.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 b3812a0..a5332eb 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -42,7 +43,6 @@
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -50,13 +50,9 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** Utility functions to manipulate Comments. */
 @Singleton
@@ -128,7 +124,7 @@
       String message,
       @Nullable Boolean unresolved,
       @Nullable String parentUuid)
-      throws OrmException, UnprocessableEntityException {
+      throws UnprocessableEntityException {
     if (unresolved == null) {
       if (parentUuid == null) {
         // Default to false if comment is not descended from another.
@@ -179,29 +175,27 @@
     return c;
   }
 
-  public Optional<Comment> getPublished(ChangeNotes notes, Comment.Key key) throws OrmException {
+  public Optional<Comment> getPublished(ChangeNotes notes, Comment.Key key) {
     return publishedByChange(notes).stream().filter(c -> key.equals(c.key)).findFirst();
   }
 
-  public Optional<Comment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key)
-      throws OrmException {
-    return draftByChangeAuthor(notes, user.getAccountId())
-        .stream()
+  public Optional<Comment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
+    return draftByChangeAuthor(notes, user.getAccountId()).stream()
         .filter(c -> key.equals(c.key))
         .findFirst();
   }
 
-  public List<Comment> publishedByChange(ChangeNotes notes) throws OrmException {
+  public List<Comment> publishedByChange(ChangeNotes notes) {
     notes.load();
     return sort(Lists.newArrayList(notes.getComments().values()));
   }
 
-  public List<RobotComment> robotCommentsByChange(ChangeNotes notes) throws OrmException {
+  public List<RobotComment> robotCommentsByChange(ChangeNotes notes) {
     notes.load();
     return sort(Lists.newArrayList(notes.getRobotComments().values()));
   }
 
-  public List<Comment> draftByChange(ChangeNotes notes) throws OrmException {
+  public List<Comment> draftByChange(ChangeNotes notes) {
     List<Comment> comments = new ArrayList<>();
     for (Ref ref : getDraftRefs(notes.getChangeId())) {
       Account.Id account = Account.Id.fromRefSuffix(ref.getName());
@@ -212,7 +206,7 @@
     return sort(comments);
   }
 
-  public List<Comment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+  public List<Comment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     List<Comment> comments = new ArrayList<>();
     comments.addAll(publishedByPatchSet(notes, psId));
 
@@ -225,18 +219,16 @@
     return sort(comments);
   }
 
-  public List<Comment> publishedByChangeFile(ChangeNotes notes, String file) throws OrmException {
+  public List<Comment> publishedByChangeFile(ChangeNotes notes, String file) {
     return commentsOnFile(notes.load().getComments().values(), file);
   }
 
-  public List<Comment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId)
-      throws OrmException {
+  public List<Comment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return removeCommentsOnAncestorOfCommitMessage(
         commentsOnPatchSet(notes.load().getComments().values(), psId));
   }
 
-  public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId)
-      throws OrmException {
+  public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
   }
 
@@ -253,18 +245,16 @@
         .collect(toList());
   }
 
-  public List<Comment> draftByPatchSetAuthor(PatchSet.Id psId, Account.Id author, ChangeNotes notes)
-      throws OrmException {
+  public List<Comment> draftByPatchSetAuthor(
+      PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
     return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
   }
 
-  public List<Comment> draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author)
-      throws OrmException {
+  public List<Comment> draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author) {
     return commentsOnFile(notes.load().getDraftComments(author).values(), file);
   }
 
-  public List<Comment> draftByChangeAuthor(ChangeNotes notes, Account.Id author)
-      throws OrmException {
+  public List<Comment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
     List<Comment> comments = new ArrayList<>();
     comments.addAll(notes.getDraftComments(author).values());
     return sort(comments);
@@ -294,26 +284,6 @@
     update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
   }
 
-  public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      for (Ref ref : getDraftRefs(repo, changeId)) {
-        bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
-      }
-      bru.setRefLogMessage("Delete drafts from NoteDb", false);
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-      for (ReceiveCommand cmd : bru.getCommands()) {
-        if (cmd.getResult() != ReceiveCommand.Result.OK) {
-          throw new IOException(
-              String.format(
-                  "Failed to delete draft comment ref %s at %s: %s (%s)",
-                  cmd.getRefName(), cmd.getOldId(), cmd.getResult(), cmd.getMessage()));
-        }
-      }
-    }
-  }
-
   private static List<Comment> commentsOnFile(Collection<Comment> allComments, String file) {
     List<Comment> result = new ArrayList<>(allComments.size());
     for (Comment c : allComments) {
@@ -368,11 +338,11 @@
    * @param changeId change ID.
    * @return raw refs from All-Users repo.
    */
-  public Collection<Ref> getDraftRefs(Change.Id changeId) throws OrmException {
+  public Collection<Ref> getDraftRefs(Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       return getDraftRefs(repo, changeId);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 94ce924..e6c46df 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -110,9 +110,7 @@
           config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
       if (createGroupsGlobal.isEmpty()) {
         createGroupAccessSection.setPermissions(
-            createGroupAccessSection
-                .getPermissions()
-                .stream()
+            createGroupAccessSection.getPermissions().stream()
                 .filter(p -> !Permission.CREATE.equals(p.getName()))
                 .collect(toList()));
         config.replace(createGroupAccessSection);
diff --git a/java/com/google/gerrit/server/DynamicOptions.java b/java/com/google/gerrit/server/DynamicOptions.java
index dcc96d5..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. */
@@ -154,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);
   }
 
   /**
@@ -196,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()));
       }
@@ -267,7 +285,7 @@
   }
 
   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();
@@ -276,14 +294,14 @@
   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;
@@ -293,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 e5e0cad..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,7 +95,7 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
+          enableReverseDnsLookup,
           Providers.of(null),
           state,
           null);
@@ -118,7 +118,7 @@
           canonicalUrl,
           accountCache,
           groupBackend,
-          disableReverseDnsLookup,
+          enableReverseDnsLookup,
           Providers.of(remotePeer),
           id,
           caller);
@@ -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;
@@ -523,7 +523,7 @@
         Providers.of(canonicalUrl.get()),
         accountCache,
         groupBackend,
-        disableReverseDnsLookup,
+        enableReverseDnsLookup,
         remotePeer,
         state,
         realUser);
@@ -554,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/ModuleOverloader.java b/java/com/google/gerrit/server/ModuleOverloader.java
index 7083e6d..9a8fe84 100644
--- a/java/com/google/gerrit/server/ModuleOverloader.java
+++ b/java/com/google/gerrit/server/ModuleOverloader.java
@@ -27,8 +27,7 @@
 
     // group candidates by annotation existence
     Map<Boolean, List<Module>> grouped =
-        overrideCandidates
-            .stream()
+        overrideCandidates.stream()
             .collect(
                 Collectors.groupingBy(m -> m.getClass().getAnnotation(ModuleImpl.class) != null));
 
@@ -44,16 +43,14 @@
     }
 
     // swipe cache implementation with alternative provided in lib
-    return modules
-        .stream()
+    return modules.stream()
         .map(
             m -> {
               ModuleImpl a = m.getClass().getAnnotation(ModuleImpl.class);
               if (a == null) {
                 return m;
               }
-              return overrides
-                  .stream()
+              return overrides.stream()
                   .filter(
                       o ->
                           o.getClass()
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 744d199c..2a78eb6 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -63,24 +62,24 @@
     this.repoManager = repoManager;
   }
 
-  public PatchSet current(ChangeNotes notes) throws OrmException {
+  public PatchSet current(ChangeNotes notes) {
     return get(notes, notes.getChange().currentPatchSetId());
   }
 
-  public PatchSet get(ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+  public PatchSet get(ChangeNotes notes, PatchSet.Id psId) {
     return notes.load().getPatchSets().get(psId);
   }
 
-  public ImmutableCollection<PatchSet> byChange(ChangeNotes notes) throws OrmException {
+  public ImmutableCollection<PatchSet> byChange(ChangeNotes notes) {
     return notes.load().getPatchSets().values();
   }
 
-  public ImmutableMap<PatchSet.Id, PatchSet> byChangeAsMap(ChangeNotes notes) throws OrmException {
+  public ImmutableMap<PatchSet.Id, PatchSet> byChangeAsMap(ChangeNotes notes) {
     return notes.load().getPatchSets();
   }
 
   public ImmutableMap<PatchSet.Id, PatchSet> getAsMap(
-      ChangeNotes notes, Set<PatchSet.Id> patchSetIds) throws OrmException {
+      ChangeNotes notes, Set<PatchSet.Id> patchSetIds) {
     return ImmutableMap.copyOf(Maps.filterKeys(notes.load().getPatchSets(), patchSetIds::contains));
   }
 
@@ -135,7 +134,7 @@
 
   /** Check if the current patch set of the change is locked. */
   public void checkPatchSetNotLocked(ChangeNotes notes)
-      throws OrmException, IOException, ResourceConflictException {
+      throws IOException, ResourceConflictException {
     if (isPatchSetLocked(notes)) {
       throw new ResourceConflictException(
           String.format("The current patch set of change %s is locked", notes.getChangeId()));
@@ -143,9 +142,9 @@
   }
 
   /** Is the current patch set locked against state changes? */
-  public boolean isPatchSetLocked(ChangeNotes notes) throws OrmException, IOException {
+  public boolean isPatchSetLocked(ChangeNotes notes) throws IOException {
     Change change = notes.getChange();
-    if (change.getStatus() == Change.Status.MERGED) {
+    if (change.isMerged()) {
       return false;
     }
 
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 25db8d9..ad93ef0 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -19,14 +19,13 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collection;
@@ -47,20 +46,19 @@
   }
 
   public void publish(
-      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag)
-      throws OrmException {
+      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag) {
     ChangeNotes notes = ctx.getNotes();
     checkArgument(notes != null);
     if (drafts.isEmpty()) {
       return;
     }
 
-    Map<Id, PatchSet> patchSets =
+    Map<PatchSet.Id, PatchSet> patchSets =
         psUtil.getAsMap(notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
     for (Comment d : drafts) {
       PatchSet ps = patchSets.get(psId(notes, d));
       if (ps == null) {
-        throw new OrmException("patch set " + ps + " not found");
+        throw new StorageException("patch set " + ps + " not found");
       }
       d.writtenOn = ctx.getWhen();
       d.tag = tag;
@@ -70,7 +68,7 @@
       try {
         CommentsUtil.setCommentRevId(d, patchListCache, notes.getChange(), ps);
       } catch (PatchListNotAvailableException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
     commentsUtil.putComments(ctx.getUpdate(psId), PUBLISHED, drafts);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index ea3cf37..676cf2e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -31,6 +31,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -46,7 +47,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -186,12 +186,11 @@
     this.queryProvider = queryProvider;
   }
 
-  public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId)
-      throws OrmException {
+  public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format(
               "Reading stars from change %d for account %d failed",
               changeId.get(), accountId.get()),
@@ -205,7 +204,7 @@
       Change.Id changeId,
       Set<String> labelsToAdd,
       Set<String> labelsToRemove)
-      throws OrmException, IllegalLabelException {
+      throws IllegalLabelException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       String refName = RefNames.refsStarredChanges(changeId, accountId);
       StarRef old = readLabels(repo, refName);
@@ -228,13 +227,22 @@
       indexer.index(project, changeId);
       return ImmutableSortedSet.copyOf(labels);
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
           e);
     }
   }
 
-  public void unstarAll(Project.NameKey project, Change.Id changeId) throws OrmException {
+  /**
+   * Unstar the given change for all users.
+   *
+   * <p>Intended for use only when we're about to delete a change. For that reason, the change is
+   * not reindexed.
+   *
+   * @param changeId change ID.
+   * @throws IOException if an error occurred.
+   */
+  public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
@@ -255,13 +263,10 @@
                   changeId.get(), command.getRefName(), command.getResult()));
         }
       }
-      indexer.index(project, changeId);
-    } catch (IOException e) {
-      throw new OrmException(String.format("Unstar change %d failed", changeId.get()), e);
     }
   }
 
-  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) throws OrmException {
+  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
       for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
@@ -274,13 +279,12 @@
       }
       return builder.build();
     } catch (IOException e) {
-      throw new OrmException(
+      throw new StorageException(
           String.format("Get accounts that starred change %d failed", changeId.get()), e);
     }
   }
 
-  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId)
-      throws OrmException {
+  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
     List<ChangeData> changeData =
         queryProvider
             .get()
@@ -294,9 +298,7 @@
 
   private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
     RefDatabase refDb = repo.getRefDatabase();
-    return refDb
-        .getRefsByPrefix(prefix)
-        .stream()
+    return refDb.getRefsByPrefix(prefix).stream()
         .map(r -> r.getName().substring(prefix.length()))
         .collect(toSet());
   }
@@ -313,7 +315,7 @@
     }
   }
 
-  public void ignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void ignore(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -322,7 +324,7 @@
         ImmutableSet.of());
   }
 
-  public void unignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void unignore(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -331,11 +333,11 @@
         ImmutableSet.of(IGNORE_LABEL));
   }
 
-  public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) throws OrmException {
+  public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) {
     return getLabels(accountId, changeId).contains(IGNORE_LABEL);
   }
 
-  public boolean isIgnored(ChangeResource rsrc) throws OrmException {
+  public boolean isIgnored(ChangeResource rsrc) {
     return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
   }
 
@@ -355,7 +357,7 @@
     return UNREVIEWED_LABEL + "/" + ps;
   }
 
-  public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void markAsReviewed(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -364,7 +366,7 @@
         ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
   }
 
-  public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void markAsUnreviewed(ChangeResource rsrc) throws IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
@@ -421,8 +423,7 @@
   }
 
   public static Set<Integer> getStarredPatchSets(Set<String> labels, String label) {
-    return labels
-        .stream()
+    return labels.stream()
         .filter(l -> l.startsWith(label + "/"))
         .filter(l -> Ints.tryParse(l.substring(label.length() + 1)) != null)
         .map(l -> Integer.valueOf(l.substring(label.length() + 1)))
@@ -447,7 +448,7 @@
 
   private void updateLabels(
       Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
-      throws IOException, OrmException, InvalidLabelsException {
+      throws IOException, InvalidLabelsException {
     try (TraceTimer traceTimer =
             TraceContext.newTimer("Update star labels in %s (labels=%s)", refName, labels);
         RevWalk rw = new RevWalk(repo)) {
@@ -474,14 +475,13 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          throw new OrmException(
+          throw new StorageException(
               String.format("Update star labels on ref %s failed: %s", refName, result.name()));
       }
     }
   }
 
-  private void deleteRef(Repository repo, String refName, ObjectId oldObjectId)
-      throws IOException, OrmException {
+  private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
     if (ObjectId.zeroId().equals(oldObjectId)) {
       // ref doesn't exist
       return;
@@ -510,7 +510,7 @@
         case REJECTED_MISSING_OBJECT:
         case REJECTED_OTHER_REASON:
         default:
-          throw new OrmException(
+          throw new StorageException(
               String.format("Delete star ref %s failed: %s", refName, result.name()));
       }
     }
diff --git a/java/com/google/gerrit/server/StartupChecks.java b/java/com/google/gerrit/server/StartupChecks.java
index 5ece91d..9bf94ae 100644
--- a/java/com/google/gerrit/server/StartupChecks.java
+++ b/java/com/google/gerrit/server/StartupChecks.java
@@ -44,7 +44,7 @@
 
   @Override
   public void start() throws StartupException {
-    startupChecks.runEach(c -> c.check(), StartupException.class);
+    startupChecks.runEach(StartupCheck::check, StartupException.class);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index d58036d..06f7a08 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -200,9 +200,9 @@
    * Creates a new account.
    *
    * @return the new account
-   * @throws OrmDuplicateKeyException if the user branch already exists
+   * @throws DuplicateKeyException if the user branch already exists
    */
-  public Account getNewAccount() throws OrmDuplicateKeyException {
+  public Account getNewAccount() throws DuplicateKeyException {
     return getNewAccount(TimeUtil.nowTs());
   }
 
@@ -210,12 +210,12 @@
    * Creates a new account.
    *
    * @return the new account
-   * @throws OrmDuplicateKeyException if the user branch already exists
+   * @throws DuplicateKeyException if the user branch already exists
    */
-  Account getNewAccount(Timestamp registeredOn) throws OrmDuplicateKeyException {
+  Account getNewAccount(Timestamp registeredOn) throws DuplicateKeyException {
     checkLoaded();
     if (revision != null) {
-      throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
+      throw new DuplicateKeyException(String.format("account %s already exists", accountId));
     }
     this.loadedAccountProperties =
         Optional.of(new AccountProperties(accountId, registeredOn, new Config(), null));
diff --git a/java/com/google/gerrit/server/account/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index 3772b4e..4b8be81 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -17,7 +17,7 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -80,7 +80,7 @@
 
   private Boolean viewAll;
 
-  AccountControl(
+  private AccountControl(
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       GroupControl.Factory groupControlFactory,
@@ -106,17 +106,6 @@
    * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective
    * groups.
    */
-  public boolean canSee(Account otherUser) {
-    return canSee(otherUser.getId());
-  }
-
-  /**
-   * Returns true if the current user is allowed to see the otherUser, based on the account
-   * visibility policy. Depending on the group membership realms supported, this may not be able to
-   * determine SAME_GROUP or VISIBLE_GROUP correctly (defaulting to not being visible). This is
-   * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective
-   * groups.
-   */
   public boolean canSee(Account.Id otherUser) {
     return canSee(
         new OtherUser() {
@@ -215,9 +204,7 @@
   }
 
   private Set<AccountGroup.UUID> groupsOf(IdentifiedUser user) {
-    return user.getEffectiveGroups()
-        .getKnownGroups()
-        .stream()
+    return user.getEffectiveGroups().getKnownGroups().stream()
         .filter(a -> !SystemGroupBackend.isSystemGroup(a))
         .collect(toSet());
   }
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index d0bd069..eb6b491 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -25,12 +25,12 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate.AccountUpdater;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
@@ -40,9 +40,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -136,21 +136,7 @@
     try {
       Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
       if (!optionalExtId.isPresent()) {
-        if (who.getUserName().isPresent()) {
-          ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, who.getUserName().get());
-          Optional<ExternalId> existingId = externalIds.get(key);
-          if (existingId.isPresent()) {
-            // An inconsistency is detected in the database, having a record for scheme "username:"
-            // but no record for scheme "gerrit:". Try to recover by linking
-            // "gerrit:" identity to the existing account.
-            logger.atWarning().log(
-                "User %s already has an account; link new identity to the existing account.",
-                who.getUserName());
-            return link(existingId.get().accountId(), who);
-          }
-        }
         // New account, automatically create and return.
-        logger.atFine().log("External ID not found. Attempting to create new account.");
         return create(who);
       }
 
@@ -177,7 +163,7 @@
       // return the identity to the caller.
       update(who, extId);
       return new AuthResult(extId.accountId(), who.getExternalIdKey(), false);
-    } catch (OrmException | ConfigInvalidException e) {
+    } catch (StorageException | ConfigInvalidException e) {
       throw new AccountException("Authentication error", e);
     }
   }
@@ -228,7 +214,7 @@
   }
 
   private void update(AuthRequest who, ExternalId extId)
-      throws OrmException, IOException, ConfigInvalidException, AccountException {
+      throws IOException, ConfigInvalidException, AccountException {
     IdentifiedUser user = userFactory.create(extId.accountId());
     List<Consumer<InternalAccountUpdate.Builder>> accountUpdates = new ArrayList<>();
 
@@ -280,12 +266,12 @@
               user.getAccountId(),
               AccountUpdater.joinConsumers(accountUpdates))
           .orElseThrow(
-              () -> new OrmException("Account " + user.getAccountId() + " has been deleted"));
+              () -> new StorageException("Account " + user.getAccountId() + " has been deleted"));
     }
   }
 
   private AuthResult create(AuthRequest who)
-      throws OrmException, AccountException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(sequences.nextAccountId());
     logger.atFine().log("Assigning new Id %s to account", newId);
 
@@ -389,7 +375,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
-      throws OrmException, IOException, ConfigInvalidException, AccountException {
+      throws IOException, ConfigInvalidException, AccountException {
     // The user initiated this request by logging in. -> Attribute all modifications to that user.
     GroupsUpdate groupsUpdate = groupsUpdateFactory.create(user);
     InternalGroupUpdate groupUpdate =
@@ -414,9 +400,8 @@
    *     this time.
    */
   public AuthResult link(Account.Id to, AuthRequest who)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
-    logger.atFine().log("Link another authentication identity to an existing account");
     if (optionalExtId.isPresent()) {
       ExternalId extId = optionalExtId.get();
       if (!extId.accountId().equals(to)) {
@@ -425,7 +410,6 @@
       }
       update(who, extId);
     } else {
-      logger.atFine().log("Linking new external ID to the existing account");
       ExternalId newExtId =
           ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
       checkEmailNotUsed(newExtId);
@@ -453,12 +437,11 @@
    * @param to account to link the identity onto.
    * @param who the additional identity.
    * @return the result of linking the identity to the user.
-   * @throws OrmException
    * @throws AccountException the identity belongs to a different account, or it cannot be linked at
    *     this time.
    */
   public AuthResult updateLink(Account.Id to, AuthRequest who)
-      throws OrmException, AccountException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     accountsUpdateProvider
         .get()
         .update(
@@ -472,8 +455,7 @@
               }
 
               if (filteredExtIdsByScheme.size() > 1
-                  || !filteredExtIdsByScheme
-                      .stream()
+                  || !filteredExtIdsByScheme.stream()
                       .anyMatch(e -> e.key().equals(who.getExternalIdKey()))) {
                 u.deleteExternalIds(filteredExtIdsByScheme);
               }
@@ -491,7 +473,7 @@
    *     found
    */
   public void unlink(Account.Id from, ExternalId.Key extIdKey)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     unlink(from, ImmutableList.of(extIdKey));
   }
 
@@ -504,7 +486,7 @@
    *     identity was not found
    */
   public void unlink(Account.Id from, Collection<ExternalId.Key> extIdKeys)
-      throws AccountException, OrmException, IOException, ConfigInvalidException {
+      throws AccountException, IOException, ConfigInvalidException {
     if (extIdKeys.isEmpty()) {
       return;
     }
@@ -530,8 +512,7 @@
             (a, u) -> {
               u.deleteExternalIds(extIds);
               if (a.getAccount().getPreferredEmail() != null
-                  && extIds
-                      .stream()
+                  && extIds.stream()
                       .anyMatch(e -> a.getAccount().getPreferredEmail().equals(e.email()))) {
                 u.setPreferredEmail(null);
               }
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 48bd1c1..2ac7147 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,272 +14,594 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static java.util.stream.Collectors.toSet;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * Helper for resolving accounts given arbitrary user-provided input.
+ *
+ * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution.
+ * The algorithm for resolving accounts from a list of formats is as follows:
+ *
+ * <ol>
+ *   <li>For each recognized format in the order listed in the method Javadoc, check whether the
+ *       input matches that format.
+ *   <li>If so, resolve accounts according to that format.
+ *   <li>Filter out invisible and inactive accounts.
+ *   <li>If the result list is non-empty, return.
+ *   <li>If the format is listed above as being short-circuiting, return.
+ *   <li>Otherwise, return to step 1 with the next format.
+ * </ol>
+ *
+ * <p>The result never includes accounts that are not visible to the calling user. It also never
+ * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
+ */
 @Singleton
 public class AccountResolver {
-  private final Provider<CurrentUser> self;
-  private final Realm realm;
-  private final Accounts accounts;
-  private final AccountCache byId;
-  private final IdentifiedUser.GenericFactory userFactory;
+  public static class UnresolvableAccountException extends UnprocessableEntityException {
+    private static final long serialVersionUID = 1L;
+    private final Result result;
+
+    @VisibleForTesting
+    UnresolvableAccountException(Result result) {
+      super(exceptionMessage(result));
+      this.result = result;
+    }
+
+    public boolean isSelf() {
+      return result.isSelf();
+    }
+  }
+
+  public static String exceptionMessage(Result result) {
+    checkArgument(result.asList().size() != 1);
+    if (result.asList().isEmpty()) {
+      if (result.isSelf()) {
+        return "Resolving account '" + result.input() + "' requires login";
+      }
+      if (result.filteredInactive().isEmpty()) {
+        return "Account '" + result.input() + "' not found";
+      }
+      return result.filteredInactive().stream()
+          .map(a -> formatForException(result, a))
+          .collect(
+              joining(
+                  "\n",
+                  "Account '"
+                      + result.input()
+                      + "' only matches inactive accounts. To use an inactive account, retry with"
+                      + " one of the following exact account IDs:\n",
+                  ""));
+    }
+
+    return result.asList().stream()
+        .map(a -> formatForException(result, a))
+        .collect(joining("\n", "Account '" + result.input() + "' is ambiguous:\n", ""));
+  }
+
+  private static String formatForException(Result result, AccountState state) {
+    return state.getAccount().getId()
+        + ": "
+        + state.getAccount().getNameEmail(result.accountResolver().anonymousCowardName);
+  }
+
+  public static boolean isSelf(String input) {
+    return "self".equals(input) || "me".equals(input);
+  }
+
+  public class Result {
+    private final String input;
+    private final ImmutableList<AccountState> list;
+    private final ImmutableList<AccountState> filteredInactive;
+
+    @VisibleForTesting
+    Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
+      this.input = requireNonNull(input);
+      this.list = canonicalize(list);
+      this.filteredInactive = canonicalize(filteredInactive);
+    }
+
+    private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
+      TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.getAccount().getId().get()));
+      set.addAll(requireNonNull(list));
+      return ImmutableList.copyOf(set);
+    }
+
+    public String input() {
+      return input;
+    }
+
+    public boolean isSelf() {
+      return AccountResolver.isSelf(input);
+    }
+
+    public ImmutableList<AccountState> asList() {
+      return list;
+    }
+
+    public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException {
+      if (list.isEmpty()) {
+        throw new UnresolvableAccountException(this);
+      }
+      return asIdSet();
+    }
+
+    public ImmutableSet<Account.Id> asIdSet() {
+      return list.stream().map(a -> a.getAccount().getId()).collect(toImmutableSet());
+    }
+
+    public AccountState asUnique() throws UnresolvableAccountException {
+      ensureUnique();
+      return list.get(0);
+    }
+
+    private void ensureUnique() throws UnresolvableAccountException {
+      if (list.size() != 1) {
+        throw new UnresolvableAccountException(this);
+      }
+    }
+
+    public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
+      ensureUnique();
+      if (isSelf()) {
+        // In the special case of "self", use the exact IdentifiedUser from the request context, to
+        // preserve the peer address and any other per-request state.
+        return self.get().asIdentifiedUser();
+      }
+      return userFactory.create(asUnique());
+    }
+
+    public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
+        throws UnresolvableAccountException {
+      ensureUnique();
+      if (isSelf()) {
+        // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
+        return self.get().asIdentifiedUser();
+      }
+      return userFactory.runAs(
+          null, list.get(0).getAccount().getId(), requireNonNull(caller).getRealUser());
+    }
+
+    @VisibleForTesting
+    ImmutableList<AccountState> filteredInactive() {
+      return filteredInactive;
+    }
+
+    private AccountResolver accountResolver() {
+      return AccountResolver.this;
+    }
+  }
+
+  @VisibleForTesting
+  interface Searcher<I> {
+    default boolean callerShouldFilterOutInactiveCandidates() {
+      return true;
+    }
+
+    default boolean callerMayAssumeCandidatesAreVisible() {
+      return false;
+    }
+
+    Optional<I> tryParse(String input) throws IOException;
+
+    Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
+
+    boolean shortCircuitIfNoResults();
+
+    default Optional<Stream<AccountState>> trySearch(String input)
+        throws IOException, ConfigInvalidException {
+      Optional<I> parsed = tryParse(input);
+      return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
+    }
+  }
+
+  @VisibleForTesting
+  abstract static class StringSearcher implements Searcher<String> {
+    @Override
+    public final Optional<String> tryParse(String input) {
+      return matches(input) ? Optional.of(input) : Optional.empty();
+    }
+
+    protected abstract boolean matches(String input);
+  }
+
+  private abstract class AccountIdSearcher implements Searcher<Account.Id> {
+    @Override
+    public final Stream<AccountState> search(Account.Id input) {
+      return Streams.stream(accountCache.get(input));
+    }
+  }
+
+  private class BySelf extends StringSearcher {
+    @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return false;
+    }
+
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true;
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return "self".equals(input) || "me".equals(input);
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      CurrentUser user = self.get();
+      if (!user.isIdentifiedUser()) {
+        return Stream.empty();
+      }
+      return Stream.of(user.asIdentifiedUser().state());
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByExactAccountId extends AccountIdSearcher {
+    @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return false;
+    }
+
+    @Override
+    public Optional<Account.Id> tryParse(String input) {
+      return Account.Id.tryParse(input);
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByParenthesizedAccountId extends AccountIdSearcher {
+    private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$");
+
+    @Override
+    public Optional<Account.Id> tryParse(String input) {
+      Matcher m = pattern.matcher(input);
+      return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByUsername extends StringSearcher {
+    @Override
+    public boolean matches(String input) {
+      return ExternalId.isValidUsername(input);
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      return Streams.stream(accountCache.getByUsername(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByNameAndEmail extends StringSearcher {
+    @Override
+    protected boolean matches(String input) {
+      int lt = input.indexOf('<');
+      int gt = input.indexOf('>');
+      return lt >= 0 && gt > lt && input.contains("@");
+    }
+
+    @Override
+    public Stream<AccountState> search(String nameOrEmail) throws IOException {
+      // TODO(dborowitz): This would probably work as a Searcher<Address>
+      int lt = nameOrEmail.indexOf('<');
+      int gt = nameOrEmail.indexOf('>');
+      Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
+      ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
+      if (allMatches.isEmpty() || allMatches.size() == 1) {
+        return allMatches.stream();
+      }
+
+      // More than one match. If there are any that match the full name as well, return only that
+      // subset. Otherwise, all are equally non-matching, so return the full set.
+      String name = nameOrEmail.substring(0, lt - 1);
+      ImmutableList<AccountState> nameMatches =
+          allMatches.stream()
+              .filter(a -> name.equals(a.getAccount().getFullName()))
+              .collect(toImmutableList());
+      return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class ByEmail extends StringSearcher {
+    @Override
+    protected boolean matches(String input) {
+      return input.contains("@");
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) throws IOException {
+      return toAccountStates(emails.getAccountFor(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return true;
+    }
+  }
+
+  private class FromRealm extends AccountIdSearcher {
+    @Override
+    public Optional<Account.Id> tryParse(String input) throws IOException {
+      return Optional.ofNullable(realm.lookup(input));
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByFullName implements Searcher<AccountState> {
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true; // Rely on enforceVisibility from the index.
+    }
+
+    @Override
+    public Optional<AccountState> tryParse(String input) {
+      List<AccountState> results =
+          accountQueryProvider.get().enforceVisibility(true).byFullName(input);
+      return results.size() == 1 ? Optional.of(results.get(0)) : Optional.empty();
+    }
+
+    @Override
+    public Stream<AccountState> search(AccountState input) {
+      return Stream.of(input);
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      return false;
+    }
+  }
+
+  private class ByDefaultSearch extends StringSearcher {
+    @Override
+    public boolean callerMayAssumeCandidatesAreVisible() {
+      return true; // Rely on enforceVisibility from the index.
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return true;
+    }
+
+    @Override
+    public Stream<AccountState> search(String input) {
+      // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
+      // up with a reasonable result list.
+      // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
+      // more strict here.
+      return accountQueryProvider.get().enforceVisibility(true).byDefault(input).stream();
+    }
+
+    @Override
+    public boolean shortCircuitIfNoResults() {
+      // In practice this doesn't matter since this is the last searcher in the list, but considered
+      // on its own, it doesn't necessarily need to be terminal.
+      return false;
+    }
+  }
+
+  private final ImmutableList<Searcher<?>> nameOrEmailSearchers =
+      ImmutableList.of(
+          new ByNameAndEmail(),
+          new ByEmail(),
+          new FromRealm(),
+          new ByFullName(),
+          new ByDefaultSearch());
+
+  private final ImmutableList<Searcher<?>> searchers =
+      ImmutableList.<Searcher<?>>builder()
+          .add(new BySelf())
+          .add(new ByExactAccountId())
+          .add(new ByParenthesizedAccountId())
+          .add(new ByUsername())
+          .addAll(nameOrEmailSearchers)
+          .build();
+
+  private final AccountCache accountCache;
   private final AccountControl.Factory accountControlFactory;
-  private final Provider<InternalAccountQuery> accountQueryProvider;
   private final Emails emails;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<CurrentUser> self;
+  private final Provider<InternalAccountQuery> accountQueryProvider;
+  private final Realm realm;
+  private final String anonymousCowardName;
 
   @Inject
   AccountResolver(
-      Provider<CurrentUser> self,
-      Realm realm,
-      Accounts accounts,
-      AccountCache byId,
-      IdentifiedUser.GenericFactory userFactory,
+      AccountCache accountCache,
+      Emails emails,
       AccountControl.Factory accountControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      Provider<CurrentUser> self,
       Provider<InternalAccountQuery> accountQueryProvider,
-      Emails emails) {
-    this.self = self;
+      Realm realm,
+      @AnonymousCowardName String anonymousCowardName) {
     this.realm = realm;
-    this.accounts = accounts;
-    this.byId = byId;
-    this.userFactory = userFactory;
+    this.accountCache = accountCache;
     this.accountControlFactory = accountControlFactory;
+    this.userFactory = userFactory;
+    this.self = self;
     this.accountQueryProvider = accountQueryProvider;
     this.emails = emails;
+    this.anonymousCowardName = anonymousCowardName;
   }
 
   /**
-   * Locate exactly one account matching the input string.
+   * Resolves all accounts matching the input string.
    *
-   * @param input a string of the format "Full Name &lt;email@example&gt;", just the email address
-   *     ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
-   *     ("username").
-   * @return the single account that matches; null if no account matches or there are multiple
-   *     candidates. If {@code input} is a numeric string, returns an account if and only if that
-   *     number corresponds to an actual account ID.
+   * <p>The following input formats are recognized:
+   *
+   * <ul>
+   *   <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
+   *       IdentifiedUser}. In this case, may return exactly one inactive account.
+   *   <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+   *       account. This case short-circuits if the input matches.
+   *   <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
+   *       case short-circuits if the input matches.
+   *   <li>A username ({@code "username"}).
+   *   <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+   *       short-circuits if the input matches.
+   *   <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+   *   <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+   *   <li>A full name ({@code "Full Name"}).
+   *   <li>As a fallback, a {@link
+   *       com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+   *       boolean, String) default search} against the account index.
+   * </ul>
+   *
+   * @param input input string.
+   * @return a result describing matching accounts. Never null even if the result set is empty.
+   * @throws ConfigInvalidException if an error occurs.
+   * @throws IOException if an error occurs.
    */
-  public Account find(String input) throws OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> r = findAll(input);
-    if (r.size() == 1) {
-      return byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null);
-    }
+  public Result resolve(String input) throws ConfigInvalidException, IOException {
+    return searchImpl(input, searchers, visibilitySupplier());
+  }
 
-    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 ConfigInvalidException if an error occurs.
+   * @throws IOException if an error occurs.
+   * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
+   *     reevaluated.
+   */
+  @Deprecated
+  public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
+    return searchImpl(input, nameOrEmailSearchers, visibilitySupplier());
+  }
+
+  private Supplier<Predicate<AccountState>> visibilitySupplier() {
+    return () -> accountControlFactory.get()::canSee;
+  }
+
+  @VisibleForTesting
+  Result searchImpl(
+      String input,
+      ImmutableList<Searcher<?>> searchers,
+      Supplier<Predicate<AccountState>> visibilitySupplier)
+      throws ConfigInvalidException, IOException {
+    visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
+    List<AccountState> inactive = new ArrayList<>();
+
+    for (Searcher<?> searcher : searchers) {
+      Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
+      if (!maybeResults.isPresent()) {
         continue;
       }
-      if (match != null) {
-        return null;
-      }
-      match = account.get();
-    }
-    return match;
-  }
+      Stream<AccountState> results = maybeResults.get();
 
-  /**
-   * Find all accounts matching the input string.
-   *
-   * @param input a string of the format "Full Name &lt;email@example&gt;", just the email address
-   *     ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
-   *     ("username").
-   * @return the accounts that match, empty set if none. Never null. If {@code input} is a numeric
-   *     string, returns a singleton set if that number corresponds to a real account ID, and an
-   *     empty set otherwise if it does not.
-   */
-  public Set<Account.Id> findAll(String input)
-      throws OrmException, IOException, ConfigInvalidException {
-    Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(input);
-    if (m.matches()) {
-      Optional<Account.Id> id = Account.Id.tryParse(m.group(1));
-      if (id.isPresent()) {
-        return Streams.stream(accounts.get(id.get()))
-            .map(a -> a.getAccount().getId())
-            .collect(toImmutableSet());
-      }
-    }
-
-    if (input.matches("^[1-9][0-9]*$")) {
-      Optional<Account.Id> id = Account.Id.tryParse(input);
-      if (id.isPresent()) {
-        return Streams.stream(accounts.get(id.get()))
-            .map(a -> a.getAccount().getId())
-            .collect(toImmutableSet());
-      }
-    }
-
-    if (ExternalId.isValidUsername(input)) {
-      Optional<AccountState> who = byId.getByUsername(input);
-      if (who.isPresent()) {
-        return ImmutableSet.of(who.map(a -> a.getAccount().getId()).get());
-      }
-    }
-
-    return findAllByNameOrEmail(input);
-  }
-
-  /**
-   * Locate exactly one account matching the name or name/email string.
-   *
-   * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
-   *     address ("email@example"), a full name ("Full Name").
-   * @return the single account that matches; null if no account matches or there are multiple
-   *     candidates.
-   */
-  public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
-    Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
-    return r.size() == 1
-        ? byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null)
-        : null;
-  }
-
-  /**
-   * Locate exactly one account matching the name or name/email string.
-   *
-   * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
-   *     address ("email@example"), a full name ("Full Name").
-   * @return the accounts that match, empty collection if none. Never null.
-   */
-  public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
-    int lt = nameOrEmail.indexOf('<');
-    int gt = nameOrEmail.indexOf('>');
-    if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
-      Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
-      if (ids.isEmpty() || ids.size() == 1) {
-        return ids;
+      if (!searcher.callerMayAssumeCandidatesAreVisible()) {
+        results = results.filter(visibilitySupplier.get());
       }
 
-      // more than one match, try to return the best one
-      String name = nameOrEmail.substring(0, lt - 1);
-      Set<Account.Id> nameMatches = new HashSet<>();
-      for (Account.Id id : ids) {
-        Optional<Account> a = byId.get(id).map(AccountState::getAccount);
-        if (a.isPresent() && name.equals(a.get().getFullName())) {
-          nameMatches.add(id);
-        }
-      }
-      return nameMatches.isEmpty() ? ids : nameMatches;
-    }
-
-    if (nameOrEmail.contains("@")) {
-      return emails.getAccountFor(nameOrEmail);
-    }
-
-    Account.Id id = realm.lookup(nameOrEmail);
-    if (id != null) {
-      return Collections.singleton(id);
-    }
-
-    List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail);
-    if (m.size() == 1) {
-      return Collections.singleton(m.get(0).getAccount().getId());
-    }
-
-    // At this point we have no clue. Just perform a whole bunch of suggestions
-    // and pray we come up with a reasonable result list.
-    // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
-    // more strict here.
-    return accountQueryProvider
-        .get()
-        .byDefault(nameOrEmail)
-        .stream()
-        .map(a -> a.getAccount().getId())
-        .collect(toSet());
-  }
-
-  /**
-   * Parses a account ID from a request body and returns the user.
-   *
-   * @param id ID of the account, can be a string of the format "{@code Full Name
-   *     <email@example.com>}", just the email address, a full name if it is unique, an account ID,
-   *     a user name or "{@code self}" for the calling user
-   * @return the user, never null.
-   * @throws UnprocessableEntityException thrown if the account ID cannot be resolved or if the
-   *     account is not visible to the calling user
-   */
-  public IdentifiedUser parse(String id)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
-    return parseOnBehalfOf(null, id);
-  }
-
-  /**
-   * Parses an account ID and returns the user without making any permission check whether the
-   * current user can see the account.
-   *
-   * @param id ID of the account, can be a string of the format "{@code Full Name
-   *     <email@example.com>}", just the email address, a full name if it is unique, an account ID,
-   *     a user name or "{@code self}" for the calling user
-   * @return the user, null if no user is found for the given account ID
-   * @throws AuthException thrown if 'self' is used as account ID and the current user is not
-   *     authenticated
-   * @throws OrmException
-   * @throws ConfigInvalidException
-   * @throws IOException
-   */
-  public IdentifiedUser parseId(String id)
-      throws AuthException, OrmException, IOException, ConfigInvalidException {
-    return parseIdOnBehalfOf(null, id);
-  }
-
-  /**
-   * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
-   */
-  public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
-    IdentifiedUser user = parseIdOnBehalfOf(caller, id);
-    if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
-      throw new UnprocessableEntityException(
-          String.format("Account '%s' is not found or ambiguous", id));
-    }
-    return user;
-  }
-
-  private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, OrmException, IOException, ConfigInvalidException {
-    if (id.equals("self")) {
-      CurrentUser user = self.get();
-      if (user.isIdentifiedUser()) {
-        return user.asIdentifiedUser();
-      } else if (user instanceof AnonymousUser) {
-        throw new AuthException("Authentication required");
+      List<AccountState> list;
+      if (searcher.callerShouldFilterOutInactiveCandidates()) {
+        // Keep track of all inactive candidates discovered by any searchers. If we end up short-
+        // circuiting, the inactive list will be discarded.
+        List<AccountState> active = new ArrayList<>();
+        results.forEach(a -> (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/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index 1a61c02..f3758bf 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -140,9 +140,7 @@
   }
 
   public static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
-    return repo.getRefDatabase()
-        .getRefsByPrefix(RefNames.REFS_USERS)
-        .stream()
+    return repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS).stream()
         .map(r -> Account.Id.fromRef(r.getName()))
         .filter(Objects::nonNull);
   }
diff --git a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
index 0b63927..6873f92 100644
--- a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
@@ -37,9 +37,7 @@
     for (AccountState accountState : accounts.all()) {
       Account account = accountState.getAccount();
       if (account.getPreferredEmail() != null) {
-        if (!accountState
-            .getExternalIds()
-            .stream()
+        if (!accountState.getExternalIds().stream()
             .anyMatch(e -> account.getPreferredEmail().equals(e.email()))) {
           addError(
               String.format(
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 5f95ecf..20a1c97 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -24,7 +24,8 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -38,14 +39,13 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+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;
@@ -80,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
@@ -122,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);
   }
 
   /**
@@ -171,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;
@@ -186,7 +198,7 @@
   // Invoked after updating the account but before committing the changes.
   private final Runnable beforeCommit;
 
-  @Inject
+  @AssistedInject
   AccountsUpdate(
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
@@ -195,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());
   }
@@ -216,7 +253,7 @@
   public AccountsUpdate(
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
-      @Nullable IdentifiedUser currentUser,
+      Optional<IdentifiedUser> currentUser,
       AllUsersName allUsersName,
       ExternalIds externalIds,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
@@ -242,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());
   }
 
   /**
@@ -256,14 +293,13 @@
    * @param accountId ID of the new account
    * @param init consumer to populate the new account
    * @return the newly created account
-   * @throws OrmDuplicateKeyException if the account already exists
+   * @throws DuplicateKeyException if the account already exists
    * @throws IOException if creating the user branch fails due to an IO error
-   * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public AccountState insert(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> init)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     return insert(message, accountId, AccountUpdater.fromConsumer(init));
   }
 
@@ -274,13 +310,12 @@
    * @param accountId ID of the new account
    * @param updater updater to populate the new account
    * @return the newly created account
-   * @throws OrmDuplicateKeyException if the account already exists
+   * @throws DuplicateKeyException if the account already exists
    * @throws IOException if creating the user branch fails due to an IO error
-   * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public AccountState insert(String message, Account.Id accountId, AccountUpdater updater)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     return updateAccount(
             r -> {
               AccountConfig accountConfig = read(r, accountId);
@@ -314,12 +349,11 @@
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws LockFailureException if updating the user branch still fails due to concurrent updates
    *     after the retry timeout exceeded
-   * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public Optional<AccountState> update(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> update)
-      throws OrmException, LockFailureException, IOException, ConfigInvalidException {
+      throws LockFailureException, IOException, ConfigInvalidException {
     return update(message, accountId, AccountUpdater.fromConsumer(update));
   }
 
@@ -335,11 +369,10 @@
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws LockFailureException if updating the user branch still fails due to concurrent updates
    *     after the retry timeout exceeded
-   * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
   public Optional<AccountState> update(String message, Account.Id accountId, AccountUpdater updater)
-      throws OrmException, LockFailureException, IOException, ConfigInvalidException {
+      throws LockFailureException, IOException, ConfigInvalidException {
     return updateAccount(
         r -> {
           AccountConfig accountConfig = read(r, accountId);
@@ -370,7 +403,7 @@
   }
 
   private Optional<AccountState> updateAccount(AccountUpdate accountUpdate)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     return executeAccountUpdate(
         () -> {
           try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
@@ -386,7 +419,7 @@
   }
 
   private Optional<AccountState> executeAccountUpdate(Action<Optional<AccountState>> action)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     try {
       return retryHelper.execute(
           ActionType.ACCOUNT_UPDATE, action, LockFailureException.class::isInstance);
@@ -394,8 +427,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -404,7 +436,7 @@
       Optional<ObjectId> rev,
       Account.Id accountId,
       InternalAccountUpdate update)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     ExternalIdNotes.checkSameAccount(
         Iterables.concat(
             update.getCreatedExternalIds(),
@@ -454,13 +486,11 @@
         .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) {
-    return batchRefUpdate
-        .getCommands()
-        .stream()
+    return batchRefUpdate.getCommands().stream()
         .map(c -> Account.Id.fromRef(c.getRefName()))
         .filter(Objects::nonNull)
         .collect(toSet());
@@ -527,8 +557,7 @@
 
   @FunctionalInterface
   private static interface AccountUpdate {
-    UpdatedAccount update(Repository allUsersRepo)
-        throws IOException, ConfigInvalidException, OrmException;
+    UpdatedAccount update(Repository allUsersRepo) throws IOException, ConfigInvalidException;
   }
 
   private static class UpdatedAccount {
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index dde6e81..33de2d2 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,11 +16,11 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -87,7 +87,7 @@
         if (1 == c.size()) {
           return c.iterator().next();
         }
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new IOException("Failed to query accounts by email", e);
       }
     }
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index e91ce49..426d6ea 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Streams;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.Action;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +69,7 @@
    *
    * @see #getAccountsFor(String...)
    */
-  public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException, OrmException {
+  public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException {
     return Streams.concat(
             externalIds.byEmail(email).stream().map(ExternalId::accountId),
             executeIndexQuery(() -> queryProvider.get().byPreferredEmail(email).stream())
@@ -83,12 +83,9 @@
    * @see #getAccountFor(String)
    */
   public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails)
-      throws IOException, OrmException {
+      throws IOException {
     ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
-    externalIds
-        .byEmails(emails)
-        .entries()
-        .stream()
+    externalIds.byEmails(emails).entries().stream()
         .forEach(e -> builder.put(e.getKey(), e.getValue().accountId()));
     executeIndexQuery(() -> queryProvider.get().byPreferredEmail(emails).entries().stream())
         .forEach(e -> builder.put(e.getKey(), e.getValue().getAccount().getId()));
@@ -105,13 +102,13 @@
     return externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
   }
 
-  private <T> T executeIndexQuery(Action<T> action) throws OrmException {
+  private <T> T executeIndexQuery(Action<T> action) {
     try {
-      return retryHelper.execute(ActionType.INDEX_QUERY, action, OrmException.class::isInstance);
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 5649629..b3e6739 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 58eaf21..c27d6c3 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -152,12 +151,9 @@
     }
 
     @Override
-    public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) throws OrmException {
+    public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) {
       try (TraceTimer timer = TraceContext.newTimer("Loading groups with member %s", memberId)) {
-        return groupQueryProvider
-            .get()
-            .byMember(memberId)
-            .stream()
+        return groupQueryProvider.get().byMember(memberId).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableSet());
       }
@@ -174,12 +170,9 @@
     }
 
     @Override
-    public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) throws OrmException {
+    public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) {
       try (TraceTimer timer = TraceContext.newTimer("Loading parent groups of %s", key)) {
-        return groupQueryProvider
-            .get()
-            .bySubgroup(key)
-            .stream()
+        return groupQueryProvider.get().bySubgroup(key).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableList());
       }
diff --git a/java/com/google/gerrit/server/account/GroupMembers.java b/java/com/google/gerrit/server/account/GroupMembers.java
index 375b3a1..d7e97ba 100644
--- a/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/java/com/google/gerrit/server/account/GroupMembers.java
@@ -127,9 +127,7 @@
     GroupControl groupControl = groupControlFactory.controlFor(new InternalGroupDescription(group));
 
     Set<Account> directMembers =
-        group
-            .getMembers()
-            .stream()
+        group.getMembers().stream()
             .filter(groupControl::canSeeMember)
             .map(accountCache::get)
             .flatMap(Streams::stream)
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/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index cc913e5..8b3e1b3 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -202,9 +202,7 @@
   private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> immutableCopyOf(
       Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
     ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyType>> b = ImmutableMap.builder();
-    projectWatches
-        .entrySet()
-        .stream()
+    projectWatches.entrySet().stream()
         .forEach(e -> b.put(e.getKey(), ImmutableSet.copyOf(e.getValue())));
     return b.build();
   }
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index a683849..da2d640 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -48,7 +47,7 @@
   }
 
   public Response<?> deactivate(Account.Id accountId)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyInactive = new AtomicBoolean(false);
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     accountsUpdateProvider
@@ -81,7 +80,7 @@
   }
 
   public Response<String> activate(Account.Id accountId)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     accountsUpdateProvider
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index e7b3c91..50a5e9f 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -218,8 +218,7 @@
     @Override
     public void check() throws StartupException {
       String invalid =
-          cfg.getSubsections("groups")
-              .stream()
+          cfg.getSubsections("groups").stream()
               .filter(
                   sub -> {
                     AccountGroup.UUID uuid = new AccountGroup.UUID(sub);
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 965f1ba..c7808de 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,7 +20,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index bfe46d2..5d12ae1 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -39,16 +39,15 @@
 
   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));
   }
 
   private static ImmutableSetMultimap<String, ExternalId> byEmailCopy(
       Collection<ExternalId> externalIds) {
-    return externalIds
-        .stream()
+    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();
@@ -62,10 +61,7 @@
     public byte[] serialize(AllExternalIds object) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       AllExternalIdsProto.Builder allBuilder = AllExternalIdsProto.newBuilder();
-      object
-          .byAccount()
-          .values()
-          .stream()
+      object.byAccount().values().stream()
           .map(extId -> toProto(idConverter, extId))
           .forEach(allBuilder::addExternalId);
       return Protos.toByteArray(allBuilder.build());
@@ -92,9 +88,7 @@
     public AllExternalIds deserialize(byte[] in) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       return create(
-          Protos.parseUnchecked(AllExternalIdsProto.parser(), in)
-              .getExternalIdList()
-              .stream()
+          Protos.parseUnchecked(AllExternalIdsProto.parser(), in).getExternalIdList().stream()
               .map(proto -> toExternalId(idConverter, proto))
               .collect(toList()));
     }
diff --git a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
index b4c82d0..aa09278 100644
--- a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
+++ b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 
 /**
  * Exception that is thrown if an external ID cannot be inserted because an external ID with the
  * same key already exists.
  */
-public class DuplicateExternalIdKeyException extends OrmDuplicateKeyException {
+public class DuplicateExternalIdKeyException extends DuplicateKeyException {
   private static final long serialVersionUID = 1L;
 
   private final ExternalId.Key duplicateKey;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 22a6ee4..c363b5b 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -82,8 +82,7 @@
    *     ExternalId#SCHEME_USERNAME} scheme
    */
   public static Optional<String> getUserName(Collection<ExternalId> extIds) {
-    return extIds
-        .stream()
+    return extIds.stream()
         .filter(e -> e.isScheme(SCHEME_USERNAME))
         .map(e -> e.key().id())
         .filter(u -> !Strings.isNullOrEmpty(u))
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 767bfd5..5aa19d8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -122,6 +122,11 @@
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
       Consumer<SetMultimap<Account.Id, ExternalId>> update) {
+    if (oldNotesRev.equals(newNotesRev)) {
+      // No need to update external id cache since there is no update to those external ids.
+      return;
+    }
+
     lock.lock();
     try {
       SetMultimap<Account.Id, ExternalId> m;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 5acf63c..41d02f5 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -656,9 +656,12 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    logger.atFine().log("Reading external ID note map");
-
-    noteMap = revision != null ? NoteMap.read(reader, revision) : NoteMap.newEmptyMap();
+    if (revision != null) {
+      logger.atFine().log("Reading external ID note map");
+      noteMap = NoteMap.read(reader, revision);
+    } else {
+      noteMap = NoteMap.newEmptyMap();
+    }
 
     if (afterReadRevision != null) {
       afterReadRevision.run();
@@ -762,8 +765,7 @@
       noteMapUpdates.clear();
       if (!footers.isEmpty()) {
         commit.setMessage(
-            footers
-                .stream()
+            footers.stream()
                 .sorted()
                 .collect(joining("\n", commit.getMessage().trim() + "\n\n", "")));
       }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index b1a59b1..9098630 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -71,8 +71,7 @@
 
   /** Returns the external IDs of the specified account that have the given scheme. */
   public Set<ExternalId> byAccount(Account.Id accountId, String scheme) throws IOException {
-    return byAccount(accountId)
-        .stream()
+    return byAccount(accountId).stream()
         .filter(e -> e.key().isScheme(scheme))
         .collect(toImmutableSet());
   }
@@ -85,8 +84,7 @@
   /** Returns the external IDs of the specified account that have the given scheme. */
   public Set<ExternalId> byAccount(Account.Id accountId, String scheme, ObjectId rev)
       throws IOException {
-    return byAccount(accountId, rev)
-        .stream()
+    return byAccount(accountId, rev).stream()
         .filter(e -> e.key().isScheme(scheme))
         .collect(toImmutableSet());
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
index 14ead2f..fe7cc48 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -93,10 +93,7 @@
       }
     }
 
-    emails
-        .asMap()
-        .entrySet()
-        .stream()
+    emails.asMap().entrySet().stream()
         .filter(e -> e.getValue().size() > 1)
         .forEach(
             e ->
@@ -104,8 +101,7 @@
                     String.format(
                         "Email '%s' is not unique, it's used by the following external IDs: %s",
                         e.getKey(),
-                        e.getValue()
-                            .stream()
+                        e.getValue().stream()
                             .map(k -> "'" + k.get() + "'")
                             .sorted()
                             .collect(joining(", "))),
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index 910ecd3..b9e26de 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -7,13 +7,15 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/restapi",
+        "//java/com/google/gerrit/util/cli",
+        "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 6f89058..d80ff9b 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -228,7 +228,7 @@
       accountLoader.fill();
       return ai;
     } catch (Exception e) {
-      throw asRestApiException("Cannot parse change", e);
+      throw asRestApiException("Cannot parse account", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 5a30113..9d29888 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -69,7 +69,7 @@
     try {
       return api.create(accounts.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id)));
     } catch (Exception e) {
-      throw asRestApiException("Cannot parse change", e);
+      throw asRestApiException("Cannot parse account", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 8588dfa..f688718 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -16,7 +16,10 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -33,6 +36,7 @@
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
@@ -48,14 +52,17 @@
 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;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.restapi.change.Abandon;
 import com.google.gerrit.server.restapi.change.ChangeIncludedIn;
@@ -66,6 +73,7 @@
 import com.google.gerrit.server.restapi.change.DeleteChange;
 import com.google.gerrit.server.restapi.change.DeletePrivate;
 import com.google.gerrit.server.restapi.change.GetAssignee;
+import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.GetHashtags;
 import com.google.gerrit.server.restapi.change.GetPastAssignees;
 import com.google.gerrit.server.restapi.change.GetPureRevert;
@@ -75,6 +83,7 @@
 import com.google.gerrit.server.restapi.change.ListChangeComments;
 import com.google.gerrit.server.restapi.change.ListChangeDrafts;
 import com.google.gerrit.server.restapi.change.ListChangeRobotComments;
+import com.google.gerrit.server.restapi.change.ListReviewers;
 import com.google.gerrit.server.restapi.change.MarkAsReviewed;
 import com.google.gerrit.server.restapi.change.MarkAsUnreviewed;
 import com.google.gerrit.server.restapi.change.Move;
@@ -89,20 +98,22 @@
 import com.google.gerrit.server.restapi.change.Revert;
 import com.google.gerrit.server.restapi.change.Reviewers;
 import com.google.gerrit.server.restapi.change.Revisions;
-import com.google.gerrit.server.restapi.change.SetPrivateOp;
 import com.google.gerrit.server.restapi.change.SetReadyForReview;
 import com.google.gerrit.server.restapi.change.SetWorkInProgress;
 import com.google.gerrit.server.restapi.change.SubmittedTogether;
 import com.google.gerrit.server.restapi.change.SuggestChangeReviewers;
 import com.google.gerrit.server.restapi.change.Unignore;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.kohsuke.args4j.CmdLineException;
 
 class ChangeApiImpl implements ChangeApi {
   interface Factory {
@@ -117,6 +128,7 @@
   private final ChangeMessageApiImpl.Factory changeMessageApi;
   private final ChangeMessages changeMessages;
   private final SuggestChangeReviewers suggestReviewers;
+  private final ListReviewers listReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
@@ -129,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;
@@ -154,6 +166,7 @@
   private final PutMessage putMessage;
   private final Provider<GetPureRevert> getPureRevertProvider;
   private final StarredChangesUtil stars;
+  private final DynamicOptionParser dynamicOptionParser;
 
   @Inject
   ChangeApiImpl(
@@ -165,6 +178,7 @@
       ChangeMessageApiImpl.Factory changeMessageApi,
       ChangeMessages changeMessages,
       SuggestChangeReviewers suggestReviewers,
+      ListReviewers listReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
@@ -176,7 +190,7 @@
       PutTopic putTopic,
       ChangeIncludedIn includedIn,
       PostReviewers postReviewers,
-      ChangeJson.Factory changeJson,
+      Provider<GetChange> getChangeProvider,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
       PutAssignee putAssignee,
@@ -201,6 +215,7 @@
       PutMessage putMessage,
       Provider<GetPureRevert> getPureRevertProvider,
       StarredChangesUtil stars,
+      DynamicOptionParser dynamicOptionParser,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -211,6 +226,7 @@
     this.changeMessageApi = changeMessageApi;
     this.changeMessages = changeMessages;
     this.suggestReviewers = suggestReviewers;
+    this.listReviewers = listReviewers;
     this.abandon = abandon;
     this.restore = restore;
     this.updateByMerge = updateByMerge;
@@ -221,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;
@@ -246,6 +262,7 @@
     this.putMessage = putMessage;
     this.getPureRevertProvider = getPureRevertProvider;
     this.stars = stars;
+    this.dynamicOptionParser = dynamicOptionParser;
     this.change = change;
   }
 
@@ -438,9 +455,23 @@
   }
 
   @Override
-  public ChangeInfo get(EnumSet<ListChangesOption> s) throws RestApiException {
+  public List<ReviewerInfo> reviewers() throws RestApiException {
     try {
-      return changeJson.create(s).format(change);
+      return listReviewers.apply(change);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve reviewers", e);
+    }
+  }
+
+  @Override
+  public ChangeInfo get(
+      EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException {
+    try {
+      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);
     }
@@ -582,7 +613,7 @@
       } else {
         unignore.apply(change, new Input());
       }
-    } catch (OrmException | IllegalLabelException e) {
+    } catch (StorageException | IllegalLabelException e) {
       throw asRestApiException("Cannot ignore change", e);
     }
   }
@@ -591,7 +622,7 @@
   public boolean ignored() throws RestApiException {
     try {
       return stars.isIgnored(change);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw asRestApiException("Cannot check if ignored", e);
     }
   }
@@ -606,7 +637,7 @@
       } else {
         markAsUnreviewed.apply(change, new Input());
       }
-    } catch (OrmException | IllegalLabelException e) {
+    } catch (StorageException | IllegalLabelException e) {
       throw asRestApiException(
           "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e);
     }
@@ -646,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/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 73f6740..ffc6524 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.extensions.api.changes.ChangeEditApi;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.client.ChangeEditDetailOption;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -33,8 +34,8 @@
 import com.google.gerrit.server.restapi.change.DeleteChangeEdit;
 import com.google.gerrit.server.restapi.change.PublishChangeEdit;
 import com.google.gerrit.server.restapi.change.RebaseChangeEdit;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.Optional;
@@ -44,51 +45,79 @@
     ChangeEditApiImpl create(ChangeResource changeResource);
   }
 
-  private final ChangeEdits.Detail editDetail;
+  private final Provider<ChangeEdits.Detail> editDetailProvider;
   private final ChangeEdits.Post changeEditsPost;
   private final DeleteChangeEdit deleteChangeEdit;
   private final RebaseChangeEdit rebaseChangeEdit;
   private final PublishChangeEdit publishChangeEdit;
-  private final ChangeEdits.Get changeEditsGet;
+  private final Provider<ChangeEdits.Get> changeEditsGetProvider;
   private final ChangeEdits.Put changeEditsPut;
   private final ChangeEdits.DeleteContent changeEditDeleteContent;
-  private final ChangeEdits.GetMessage getChangeEditCommitMessage;
+  private final Provider<ChangeEdits.GetMessage> getChangeEditCommitMessageProvider;
   private final ChangeEdits.EditMessage modifyChangeEditCommitMessage;
   private final ChangeEdits changeEdits;
   private final ChangeResource changeResource;
 
   @Inject
   public ChangeEditApiImpl(
-      ChangeEdits.Detail editDetail,
+      Provider<ChangeEdits.Detail> editDetailProvider,
       ChangeEdits.Post changeEditsPost,
       DeleteChangeEdit deleteChangeEdit,
       RebaseChangeEdit rebaseChangeEdit,
       PublishChangeEdit publishChangeEdit,
-      ChangeEdits.Get changeEditsGet,
+      Provider<ChangeEdits.Get> changeEditsGetProvider,
       ChangeEdits.Put changeEditsPut,
       ChangeEdits.DeleteContent changeEditDeleteContent,
-      ChangeEdits.GetMessage getChangeEditCommitMessage,
+      Provider<ChangeEdits.GetMessage> getChangeEditCommitMessageProvider,
       ChangeEdits.EditMessage modifyChangeEditCommitMessage,
       ChangeEdits changeEdits,
       @Assisted ChangeResource changeResource) {
-    this.editDetail = editDetail;
+    this.editDetailProvider = editDetailProvider;
     this.changeEditsPost = changeEditsPost;
     this.deleteChangeEdit = deleteChangeEdit;
     this.rebaseChangeEdit = rebaseChangeEdit;
     this.publishChangeEdit = publishChangeEdit;
-    this.changeEditsGet = changeEditsGet;
+    this.changeEditsGetProvider = changeEditsGetProvider;
     this.changeEditsPut = changeEditsPut;
     this.changeEditDeleteContent = changeEditDeleteContent;
-    this.getChangeEditCommitMessage = getChangeEditCommitMessage;
+    this.getChangeEditCommitMessageProvider = getChangeEditCommitMessageProvider;
     this.modifyChangeEditCommitMessage = modifyChangeEditCommitMessage;
     this.changeEdits = changeEdits;
     this.changeResource = changeResource;
   }
 
   @Override
+  public ChangeEditDetailRequest detail() throws RestApiException {
+    try {
+      return new ChangeEditDetailRequest() {
+        @Override
+        public Optional<EditInfo> get() throws RestApiException {
+          return ChangeEditApiImpl.this.get(this);
+        }
+      };
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve change edit", e);
+    }
+  }
+
+  private Optional<EditInfo> get(ChangeEditDetailRequest r) throws RestApiException {
+    try {
+      ChangeEdits.Detail editDetail = editDetailProvider.get();
+      editDetail.setBase(r.getBase());
+      editDetail.setList(r.options().contains(ChangeEditDetailOption.LIST_FILES));
+      editDetail.setDownloadCommands(
+          r.options().contains(ChangeEditDetailOption.DOWNLOAD_COMMANDS));
+      Response<EditInfo> edit = editDetail.apply(changeResource);
+      return edit.isNone() ? Optional.empty() : Optional.of(edit.value());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve change edit", e);
+    }
+  }
+
+  @Override
   public Optional<EditInfo> get() throws RestApiException {
     try {
-      Response<EditInfo> edit = editDetail.apply(changeResource);
+      Response<EditInfo> edit = editDetailProvider.get().apply(changeResource);
       return edit.isNone() ? Optional.empty() : Optional.of(edit.value());
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve change edit", e);
@@ -140,7 +169,7 @@
   public Optional<BinaryResult> getFile(String filePath) throws RestApiException {
     try {
       ChangeEditResource changeEditResource = getChangeEditResource(filePath);
-      Response<BinaryResult> fileResponse = changeEditsGet.apply(changeEditResource);
+      Response<BinaryResult> fileResponse = changeEditsGetProvider.get().apply(changeEditResource);
       return fileResponse.isNone() ? Optional.empty() : Optional.of(fileResponse.value());
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve file of change edit", e);
@@ -191,7 +220,8 @@
   @Override
   public String getCommitMessage() throws RestApiException {
     try {
-      try (BinaryResult binaryResult = getChangeEditCommitMessage.apply(changeResource)) {
+      try (BinaryResult binaryResult =
+          getChangeEditCommitMessageProvider.get().apply(changeResource)) {
         return binaryResult.asString();
       }
     } catch (Exception e) {
@@ -211,7 +241,7 @@
   }
 
   private ChangeEditResource getChangeEditResource(String filePath)
-      throws ResourceNotFoundException, AuthException, IOException, OrmException {
+      throws ResourceNotFoundException, AuthException, IOException {
     return changeEdits.parse(changeResource, IdString.fromDecoded(filePath));
   }
 }
diff --git a/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 2f19040..acff137 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.api.changes.ChangeApiImpl.DynamicOptionParser;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.CreateChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
@@ -43,6 +44,7 @@
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
   private final CreateChange createChange;
+  private final DynamicOptionParser dynamicOptionParser;
   private final Provider<QueryChanges> queryProvider;
 
   @Inject
@@ -50,10 +52,12 @@
       ChangesCollection changes,
       ChangeApiImpl.Factory api,
       CreateChange createChange,
+      DynamicOptionParser dynamicOptionParser,
       Provider<QueryChanges> queryProvider) {
     this.changes = changes;
     this.api = api;
     this.createChange = createChange;
+    this.dynamicOptionParser = dynamicOptionParser;
     this.queryProvider = queryProvider;
   }
 
@@ -116,9 +120,11 @@
     }
     qc.setLimit(q.getLimit());
     qc.setStart(q.getStart());
+    qc.setNoLimit(q.getNoLimit());
     for (ListChangesOption option : q.getOptions()) {
       qc.addOption(option);
     }
+    dynamicOptionParser.parseDynamicOptions(qc, q.getPluginOptions());
 
     try {
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
diff --git a/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 6e18bb8..f2d0ef8 100644
--- a/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -18,11 +18,13 @@
 
 import com.google.gerrit.extensions.api.changes.FileApi;
 import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.restapi.change.GetContent;
 import com.google.gerrit.server.restapi.change.GetDiff;
+import com.google.gerrit.server.restapi.change.Reviewed;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -33,12 +35,21 @@
 
   private final GetContent getContent;
   private final GetDiff getDiff;
+  private final Reviewed.PutReviewed putReviewed;
+  private final Reviewed.DeleteReviewed deleteReviewed;
   private final FileResource file;
 
   @Inject
-  FileApiImpl(GetContent getContent, GetDiff getDiff, @Assisted FileResource file) {
+  FileApiImpl(
+      GetContent getContent,
+      GetDiff getDiff,
+      Reviewed.PutReviewed putReviewed,
+      Reviewed.DeleteReviewed deleteReviewed,
+      @Assisted FileResource file) {
     this.getContent = getContent;
     this.getDiff = getDiff;
+    this.putReviewed = putReviewed;
+    this.deleteReviewed = deleteReviewed;
     this.file = file;
   }
 
@@ -88,6 +99,19 @@
     };
   }
 
+  @Override
+  public void setReviewed(boolean reviewed) throws RestApiException {
+    try {
+      if (reviewed) {
+        putReviewed.apply(file, new Input());
+      } else {
+        deleteReviewed.apply(file, new Input());
+      }
+    } catch (Exception e) {
+      throw asRestApiException(String.format("Cannot set %sreviewed", reviewed ? "" : "un"), e);
+    }
+  }
+
   private DiffInfo get(DiffRequest r) throws RestApiException {
     if (r.getBase() != null) {
       getDiff.setBase(r.getBase());
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index f8a2ecb..2df7ae6 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -18,6 +18,8 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 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;
@@ -36,6 +38,7 @@
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -51,6 +54,10 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.RebaseUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -85,6 +92,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -135,6 +143,8 @@
   private final GetRelated getRelated;
   private final PutDescription putDescription;
   private final GetDescription getDescription;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
   RevisionApiImpl(
@@ -176,6 +186,8 @@
       GetRelated getRelated,
       PutDescription putDescription,
       GetDescription getDescription,
+      ApprovalsUtil approvalsUtil,
+      AccountLoader.Factory accountLoaderFactory,
       @Assisted RevisionResource r) {
     this.repoManager = repoManager;
     this.changes = changes;
@@ -215,6 +227,8 @@
     this.getRelated = getRelated;
     this.putDescription = putDescription;
     this.getDescription = getDescription;
+    this.approvalsUtil = approvalsUtil;
+    this.accountLoaderFactory = accountLoaderFactory;
     this.revision = r;
   }
 
@@ -568,6 +582,36 @@
   }
 
   @Override
+  public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+    ListMultimap<String, ApprovalInfo> result =
+        ListMultimapBuilder.treeKeys().arrayListValues().build();
+    try {
+      Iterable<PatchSetApproval> approvals =
+          approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().getId(), null, null);
+      AccountLoader accountLoader =
+          accountLoaderFactory.create(
+              EnumSet.of(
+                  FillOptions.ID, FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME));
+      for (PatchSetApproval approval : approvals) {
+        String label = approval.getLabel();
+        ApprovalInfo info =
+            new ApprovalInfo(
+                approval.getAccountId().get(),
+                Integer.valueOf(approval.getValue()),
+                null,
+                approval.getTag(),
+                approval.getGranted());
+        accountLoader.put(info);
+        result.get(label).add(info);
+      }
+      accountLoader.fill();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get votes", e);
+    }
+    return result;
+  }
+
+  @Override
   public void description(String description) throws RestApiException {
     DescriptionInput in = new DescriptionInput();
     in.description = description;
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/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 463c23e..354331e 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -70,7 +70,6 @@
 import com.google.gerrit.server.restapi.project.GetParent;
 import com.google.gerrit.server.restapi.project.Index;
 import com.google.gerrit.server.restapi.project.ListBranches;
-import com.google.gerrit.server.restapi.project.ListChildProjects;
 import com.google.gerrit.server.restapi.project.ListDashboards;
 import com.google.gerrit.server.restapi.project.ListTags;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
@@ -475,10 +474,17 @@
 
   @Override
   public List<ProjectInfo> children(boolean recursive) throws RestApiException {
-    ListChildProjects list = children.list();
-    list.setRecursive(recursive);
     try {
-      return list.apply(checkExists());
+      return children.list().withRecursive(recursive).apply(checkExists());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list children", e);
+    }
+  }
+
+  @Override
+  public List<ProjectInfo> children(int limit) throws RestApiException {
+    try {
+      return children.list().withLimit(limit).apply(checkExists());
     } catch (Exception e) {
       throw asRestApiException("Cannot list children", e);
     }
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 4552e7a..5d25d1a 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -16,20 +16,19 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects;
 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.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.ListProjects;
 import com.google.gerrit.server.restapi.project.ListProjects.FilterType;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.restapi.project.QueryProjects;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -118,9 +117,6 @@
       case CODE:
         type = FilterType.CODE;
         break;
-      case PARENT_CANDIDATES:
-        type = FilterType.PARENT_CANDIDATES;
-        break;
       case PERMISSIONS:
         type = FilterType.PERMISSIONS;
         break;
@@ -153,13 +149,13 @@
 
   private List<ProjectInfo> query(QueryRequest r) throws RestApiException {
     try {
-      QueryProjects myQueryProjects = queryProvider.get();
-      myQueryProjects.setQuery(r.getQuery());
-      myQueryProjects.setLimit(r.getLimit());
-      myQueryProjects.setStart(r.getStart());
-
-      return myQueryProjects.apply(TopLevelResource.INSTANCE);
-    } catch (OrmException e) {
+      return queryProvider
+          .get()
+          .withQuery(r.getQuery())
+          .withLimit(r.getLimit())
+          .withStart(r.getStart())
+          .apply();
+    } catch (StorageException e) {
       throw new RestApiException("Cannot query projects", e);
     }
   }
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 2b66334..efc1866 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -16,7 +16,9 @@
 
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -24,7 +26,6 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -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:
@@ -81,7 +81,7 @@
             throw new CmdLineException(owner, localizable("user \"%s\" not found"), token);
         }
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new CmdLineException(owner, localizable("database is down"));
     } catch (IOException e) {
       throw new CmdLineException(owner, "Failed to load account", e);
diff --git a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index 13832fa..77f0fd8 100644
--- a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -17,12 +17,12 @@
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
 import com.google.common.base.Splitter;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -67,7 +67,7 @@
       }
     } catch (IllegalArgumentException e) {
       throw new CmdLineException(owner, localizable("Change-Id is not valid"));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new CmdLineException(owner, localizable("Database error: %s"), e.getMessage());
     }
 
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 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 f66c129..fc8a3cc 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:gwtorm",
         "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 0980116..78b58e1 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
@@ -27,7 +28,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.IntKeyCacheSerializer;
+import com.google.gerrit.server.cache.serialize.IntegerCacheSerializer;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -45,7 +46,10 @@
       protected void configure() {
         persist(OAUTH_TOKENS, Account.Id.class, OAuthToken.class)
             .version(1)
-            .keySerializer(new IntKeyCacheSerializer<>(Account.Id::new))
+            .keySerializer(
+                CacheSerializer.convert(
+                    IntegerCacheSerializer.INSTANCE,
+                    Converter.from(Account.Id::get, Account.Id::new)))
             .valueSerializer(new Serializer());
       }
     };
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index a8fb53b..3732e37 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -345,8 +345,7 @@
             }
           }
         } catch (Exception e) {
-          if (Throwables.getCausalChain(e)
-              .stream()
+          if (Throwables.getCausalChain(e).stream()
               .anyMatch(InvalidClassException.class::isInstance)) {
             // If deserialization failed using default Java serialization, this means we are using
             // the old serialVersionUID-based invalidation strategy. In that case, authors are
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index cd9912c..9600074 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -5,8 +5,8 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/proto",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
diff --git a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
index 2d41f2c..5377fc1 100644
--- a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.cache.serialize;
 
+import com.google.common.base.Converter;
+
 /**
  * Interface for serializing/deserializing a type to/from a persistent cache.
  *
@@ -22,6 +24,27 @@
  */
 public interface CacheSerializer<T> {
   /**
+   * Convert a serializer of one type to another type using a {@link Converter}.
+   *
+   * @param delegate underlying serializer.
+   * @param converter converter between an arbitrary type {@code T} and {@code delegate}'s type.
+   * @return serializer of type {@code T}.
+   */
+  static <T, D> CacheSerializer<T> convert(CacheSerializer<D> delegate, Converter<T, D> converter) {
+    return new CacheSerializer<T>() {
+      @Override
+      public byte[] serialize(T object) {
+        return delegate.serialize(converter.convert(object));
+      }
+
+      @Override
+      public T deserialize(byte[] in) {
+        return converter.reverse().convert(delegate.deserialize(in));
+      }
+    };
+  }
+
+  /**
    * Serializes the object to a new byte array.
    *
    * @param object object to serialize.
diff --git a/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
deleted file mode 100644
index 85530f4..0000000
--- a/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache.serialize;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.gwtorm.client.IntKey;
-import java.util.function.Function;
-
-public class IntKeyCacheSerializer<K extends IntKey<?>> implements CacheSerializer<K> {
-  private final Function<Integer, K> factory;
-
-  public IntKeyCacheSerializer(Function<Integer, K> factory) {
-    this.factory = requireNonNull(factory);
-  }
-
-  @Override
-  public byte[] serialize(K object) {
-    return IntegerCacheSerializer.INSTANCE.serialize(object.get());
-  }
-
-  @Override
-  public K deserialize(byte[] in) {
-    return factory.apply(IntegerCacheSerializer.INSTANCE.deserialize(in));
-  }
-}
diff --git a/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
new file mode 100644
index 0000000..180646b
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize;
+
+import com.google.gerrit.proto.Protos;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+/** A CacheSerializer for Protobuf messages. */
+public class ProtobufSerializer<T extends MessageLite> implements CacheSerializer<T> {
+  private final Parser<T> parser;
+
+  public ProtobufSerializer(Parser<T> parser) {
+    this.parser = parser;
+  }
+
+  @Override
+  public byte[] serialize(T object) {
+    return Protos.toByteArray(object);
+  }
+
+  @Override
+  public T deserialize(byte[] in) {
+    return Protos.parseUnchecked(parser, in);
+  }
+}
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 3999955..5ee5bc7 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -15,13 +15,9 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,7 +32,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -49,8 +44,6 @@
   private final ChangeAbandoned changeAbandoned;
 
   private final String msgTxt;
-  private final NotifyHandling notifyHandling;
-  private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
   private final AccountState accountState;
 
   private Change change;
@@ -59,10 +52,7 @@
 
   public interface Factory {
     AbandonOp create(
-        @Assisted @Nullable AccountState accountState,
-        @Assisted @Nullable String msgTxt,
-        @Assisted NotifyHandling notifyHandling,
-        @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        @Assisted @Nullable AccountState accountState, @Assisted @Nullable String msgTxt);
   }
 
   @Inject
@@ -72,9 +62,7 @@
       PatchSetUtil psUtil,
       ChangeAbandoned changeAbandoned,
       @Assisted @Nullable AccountState accountState,
-      @Assisted @Nullable String msgTxt,
-      @Assisted NotifyHandling notifyHandling,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+      @Assisted @Nullable String msgTxt) {
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
@@ -82,8 +70,6 @@
 
     this.accountState = accountState;
     this.msgTxt = Strings.nullToEmpty(msgTxt);
-    this.notifyHandling = notifyHandling;
-    this.accountsToNotify = accountsToNotify;
   }
 
   @Nullable
@@ -92,11 +78,11 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, ResourceConflictException {
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException {
     change = ctx.getChange();
     PatchSet.Id psId = change.currentPatchSetId();
     ChangeUpdate update = ctx.getUpdate(psId);
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
     patchSet = psUtil.get(ctx.getNotes(), psId);
@@ -121,19 +107,19 @@
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
       if (accountState != null) {
         cm.setFrom(accountState.getAccount().getId());
       }
       cm.setChangeMessage(message.getMessage(), ctx.getWhen());
-      cm.setNotify(notifyHandling);
-      cm.setAccountsToNotify(accountsToNotify);
+      cm.setNotify(notify);
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
     }
-    changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notifyHandling);
+    changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notify.handling());
   }
 }
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index f505f6d..6f46498 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.InternalUser;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -96,14 +96,14 @@
         }
       }
       logger.atInfo().log("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size());
-    } catch (QueryParseException | OrmException e) {
+    } catch (QueryParseException | StorageException e) {
       logger.atSevere().withCause(e).log(
           "Failed to query inactive open changes for auto-abandoning.");
     }
   }
 
   private Collection<ChangeData> getValidChanges(Collection<ChangeData> changes, String query)
-      throws OrmException, QueryParseException {
+      throws QueryParseException {
     Collection<ChangeData> validChanges = new ArrayList<>();
     for (ChangeData cd : changes) {
       String newQuery = query + " change:" + cd.getId();
diff --git a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
index 69825ea..fc3e476 100644
--- a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.Optional;
 
@@ -54,9 +53,8 @@
    * @param path file path
    * @return {@code true} if the reviewed flag was updated, {@code false} if the reviewed flag was
    *     already set
-   * @throws OrmException thrown if updating the reviewed flag failed
    */
-  boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) throws OrmException;
+  boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path);
 
   /**
    * Marks the given files in the given patch set as reviewed by the given user.
@@ -64,10 +62,8 @@
    * @param psId patch set ID
    * @param accountId account ID of the user
    * @param paths file paths
-   * @throws OrmException thrown if updating the reviewed flag failed
    */
-  void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
-      throws OrmException;
+  void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths);
 
   /**
    * Clears the reviewed flag for the given file in the given patch set for the given user.
@@ -75,17 +71,15 @@
    * @param psId patch set ID
    * @param accountId account ID of the user
    * @param path file path
-   * @throws OrmException thrown if clearing the reviewed flag failed
    */
-  void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) throws OrmException;
+  void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path);
 
   /**
    * Clears the reviewed flags for all files in the given patch set for all users.
    *
    * @param psId patch set ID
-   * @throws OrmException thrown if clearing the reviewed flags failed
    */
-  void clearReviewed(PatchSet.Id psId) throws OrmException;
+  void clearReviewed(PatchSet.Id psId);
 
   /**
    * Find the latest patch set, that is smaller or equals to the given patch set, where at least,
@@ -95,8 +89,6 @@
    * @param accountId account ID of the user
    * @return optionally, all files the have been reviewed by the given user that belong to the patch
    *     set that is smaller or equals to the given patch set
-   * @throws OrmException thrown if accessing the reviewed flags failed
    */
-  Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
-      throws OrmException;
+  Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId);
 }
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 0a9fe81..d493b31 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -29,11 +29,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -71,7 +69,7 @@
     this.userProvider = userProvider;
   }
 
-  public Map<String, ActionInfo> format(RevisionResource rsrc) throws OrmException {
+  public Map<String, ActionInfo> format(RevisionResource rsrc) {
     ChangeInfo changeInfo = null;
     RevisionInfo revisionInfo = null;
     List<ActionVisitor> visitors = visitors();
@@ -98,7 +96,7 @@
   }
 
   public RevisionInfo addRevisionActions(
-      @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) throws OrmException {
+      @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
     List<ActionVisitor> visitors = visitors();
     if (!visitors.isEmpty()) {
       if (changeInfo != null) {
@@ -180,8 +178,7 @@
     // The followup action is a client-side only operation that does not
     // have a server side handler. It must be manually registered into the
     // resulting action map.
-    Status status = notes.getChange().getStatus();
-    if (status.isOpen() || status.equals(Status.MERGED)) {
+    if (!notes.getChange().isAbandoned()) {
       UiAction.Description descr = new UiAction.Description();
       PrivateInternals_UiActionDescription.setId(descr, "followup");
       PrivateInternals_UiActionDescription.setMethod(descr, "POST");
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index 4173950..d9c5dad 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -16,12 +16,8 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,9 +45,7 @@
       Collection<Account.Id> copied,
       Collection<Address> addedByEmail,
       Collection<Address> copiedByEmail,
-      NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean readyForReview) {
+      NotifyResolver.Result notify) {
     // The user knows they added themselves, don't bother emailing them.
     Account.Id userId = user.getAccountId();
     ImmutableList<Account.Id> toMail =
@@ -64,11 +58,7 @@
 
     try {
       AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId());
-      // Default to silent operation on WIP changes.
-      NotifyHandling defaultNotifyHandling =
-          readyForReview ? NotifyHandling.ALL : NotifyHandling.NONE;
-      cm.setNotify(MoreObjects.firstNonNull(notify, defaultNotifyHandling));
-      cm.setAccountsToNotify(accountsToNotify);
+      cm.setNotify(notify);
       cm.setFrom(userId);
       cm.addReviewers(toMail);
       cm.addReviewersByEmail(addedByEmail);
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index ea42652..610290d 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -25,12 +25,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.mail.Address;
@@ -48,7 +44,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -69,16 +64,10 @@
      * @param accountIds account IDs to add.
      * @param addresses email addresses to add.
      * @param state resulting reviewer state.
-     * @param notify notification handling.
-     * @param accountsToNotify additional accounts to notify.
      * @return batch update operation.
      */
     AddReviewersOp create(
-        Set<Account.Id> accountIds,
-        Collection<Address> addresses,
-        ReviewerState state,
-        @Nullable NotifyHandling notify,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
   }
 
   @AutoValue
@@ -118,8 +107,6 @@
   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.
@@ -128,6 +115,7 @@
   private Collection<Account.Id> addedCCs = ImmutableList.of();
   private Collection<Address> addedCCsByEmail = ImmutableList.of();
 
+  private boolean sendEmail = true;
   private Change change;
   private PatchSet patchSet;
   private Result opResult;
@@ -142,9 +130,7 @@
       AddReviewersEmail addReviewersEmail,
       @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;
@@ -156,8 +142,13 @@
     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) {
@@ -165,8 +156,7 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
     change = ctx.getChange();
     if (!accountIds.isEmpty()) {
       if (state == CC) {
@@ -246,20 +236,19 @@
             .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()
+          addedReviewers.stream()
               .map(r -> accountCache.get(r.getAccountId()))
               .flatMap(Streams::stream)
               .collect(toList());
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index b15db60..8c67531 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,13 +14,8 @@
 
 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.server.CurrentUser;
 import com.google.gerrit.server.account.AccountState;
@@ -54,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(project, user, TimeUtil.nowTs())) {
+      u.setNotify(notify);
       for (ChangeData change : changes) {
         if (!project.equals(change.project())) {
           throw new ResourceConflictException(
@@ -69,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();
     }
@@ -84,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(
@@ -100,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/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index 5e7a9bf..4ad3c67 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.DeprecatedIdentifierException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.IndexConfig;
@@ -37,7 +38,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -106,7 +106,7 @@
     this.allowedIdTypes = ImmutableSet.copyOf(configuredChangeIdTypes);
   }
 
-  public ChangeNotes findOne(String id) throws OrmException {
+  public ChangeNotes findOne(String id) {
     List<ChangeNotes> ctls = find(id);
     if (ctls.size() != 1) {
       return null;
@@ -119,14 +119,13 @@
    *
    * @param id change identifier.
    * @return possibly-empty list of notes for all matching changes; may or may not be visible.
-   * @throws OrmException if an error occurred querying the database.
    */
-  public List<ChangeNotes> find(String id) throws OrmException {
+  public List<ChangeNotes> find(String id) {
     try {
       return find(id, false);
     } catch (DeprecatedIdentifierException e) {
       // This can't happen because we don't enforce deprecation
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
@@ -137,11 +136,10 @@
    * @param enforceDeprecation boolean to see if we should throw {@link
    *     DeprecatedIdentifierException} in case the identifier is deprecated
    * @return possibly-empty list of notes for all matching changes; may or may not be visible.
-   * @throws OrmException if an error occurred querying the database
    * @throws DeprecatedIdentifierException if the identifier is deprecated.
    */
   public List<ChangeNotes> find(String id, boolean enforceDeprecation)
-      throws OrmException, DeprecatedIdentifierException {
+      throws DeprecatedIdentifierException {
     if (id.isEmpty()) {
       return Collections.emptyList();
     }
@@ -194,17 +192,16 @@
     return notes;
   }
 
-  private List<ChangeNotes> fromProjectNumber(String project, int changeNumber)
-      throws OrmException {
+  private List<ChangeNotes> fromProjectNumber(String project, int changeNumber) {
     Change.Id cId = new Change.Id(changeNumber);
     try {
       return ImmutableList.of(
           changeNotesFactory.createChecked(Project.NameKey.parse(project), cId));
     } catch (NoSuchChangeException e) {
       return Collections.emptyList();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       // Distinguish between a RepositoryNotFoundException (project argument invalid) and
-      // other OrmExceptions (failure in the persistence layer).
+      // other StorageExceptions (failure in the persistence layer).
       if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) {
         return Collections.emptyList();
       }
@@ -212,7 +209,7 @@
     }
   }
 
-  public ChangeNotes findOne(Change.Id id) throws OrmException {
+  public ChangeNotes findOne(Change.Id id) {
     List<ChangeNotes> notes = find(id);
     if (notes.size() != 1) {
       throw new NoSuchChangeException(id);
@@ -220,7 +217,7 @@
     return notes.get(0);
   }
 
-  public List<ChangeNotes> find(Change.Id id) throws OrmException {
+  public List<ChangeNotes> find(Change.Id id) {
     String project = changeIdProjectCache.getIfPresent(id);
     if (project != null) {
       return fromProjectNumber(project, id.get());
@@ -236,7 +233,7 @@
     return asChangeNotes(r);
   }
 
-  private List<ChangeNotes> asChangeNotes(List<ChangeData> cds) throws OrmException {
+  private List<ChangeNotes> asChangeNotes(List<ChangeData> cds) {
     List<ChangeNotes> notes = new ArrayList<>(cds.size());
     if (!indexConfig.separateChangeSubIndexes()) {
       for (ChangeData cd : cds) {
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index fb92ec9..9f9ec1f 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -24,16 +24,13 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -71,7 +68,6 @@
 import com.google.gerrit.server.update.InsertChangeOp;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -123,8 +119,6 @@
   private boolean workInProgress;
   private List<String> groups = Collections.emptyList();
   private boolean validate = true;
-  private NotifyHandling notify = NotifyHandling.ALL;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
   private Map<String, Short> approvals;
   private RequestScopePropagator requestScopePropagator;
   private boolean fireRevisionCreated;
@@ -255,17 +249,6 @@
     return this;
   }
 
-  public ChangeInserter setNotify(NotifyHandling notify) {
-    this.notify = notify;
-    return this;
-  }
-
-  public ChangeInserter setAccountsToNotify(
-      ListMultimap<RecipientType, Account.Id> accountsToNotify) {
-    this.accountsToNotify = requireNonNull(accountsToNotify);
-    return this;
-  }
-
   public ChangeInserter setReviewersAndCcs(
       Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
     return setReviewersAndCcsAsStrings(
@@ -385,8 +368,7 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     change = ctx.getChange(); // Use defensive copy created by ChangeControl.
     patchSetInfo =
         patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
@@ -457,7 +439,8 @@
   @Override
   public void postUpdate(Context ctx) throws Exception {
     reviewerAdditions.postUpdate(ctx);
-    if (sendMail && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) {
+    NotifyResolver.Result notify = ctx.getNotify(change.getId());
+    if (sendMail && notify.shouldNotify()) {
       Runnable sender =
           new Runnable() {
             @Override
@@ -468,11 +451,8 @@
                 cm.setFrom(change.getOwner());
                 cm.setPatchSet(patchSet, patchSetInfo);
                 cm.setNotify(notify);
-                cm.setAccountsToNotify(accountsToNotify);
                 cm.addReviewers(
-                    reviewerAdditions
-                        .flattenResults(AddReviewersOp.Result::addedReviewers)
-                        .stream()
+                    reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
                         .map(PatchSetApproval::getAccountId)
                         .collect(toImmutableSet()));
                 cm.addReviewersByEmail(
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 28f9618..9e43cee 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.common.data.SubmitRecord.Status;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
@@ -83,6 +84,7 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -95,8 +97,6 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
-import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -111,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}.
@@ -216,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;
@@ -236,6 +238,7 @@
       TrackingFooters trackingFooters,
       Metrics metrics,
       RevisionJson.Factory revisionJsonFactory,
+      @GerritServerConfig Config cfg,
       @Assisted Iterable<ListChangesOption> options,
       @Assisted Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory) {
     this.userProvider = user;
@@ -252,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;
 
@@ -263,23 +268,23 @@
     return this;
   }
 
-  public ChangeInfo format(ChangeResource rsrc) throws OrmException {
+  public ChangeInfo format(ChangeResource rsrc) {
     return format(changeDataFactory.create(rsrc.getNotes()));
   }
 
-  public ChangeInfo format(Change change) throws OrmException {
+  public ChangeInfo format(Change change) {
     return format(changeDataFactory.create(change));
   }
 
-  public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException {
+  public ChangeInfo format(Project.NameKey project, Change.Id id) {
     return format(project, id, ChangeInfo::new);
   }
 
-  public ChangeInfo format(ChangeData cd) throws OrmException {
+  public ChangeInfo format(ChangeData cd) {
     return format(cd, Optional.empty(), true, ChangeInfo::new);
   }
 
-  public ChangeInfo format(RevisionResource rsrc) throws OrmException {
+  public ChangeInfo format(RevisionResource rsrc) {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
   }
@@ -303,8 +308,7 @@
     }
   }
 
-  public List<ChangeInfo> format(Collection<ChangeData> in)
-      throws OrmException, PermissionBackendException {
+  public List<ChangeInfo> format(Collection<ChangeData> in) throws PermissionBackendException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     ensureLoaded(in);
     List<ChangeInfo> out = new ArrayList<>(in.size());
@@ -316,11 +320,11 @@
   }
 
   public <I extends ChangeInfo> I format(
-      Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) throws OrmException {
+      Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) {
     ChangeNotes notes;
     try {
       notes = notesFactory.createChecked(project, id);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       if (!has(CHECK)) {
         throw e;
       }
@@ -361,8 +365,7 @@
       ChangeData cd,
       Optional<PatchSet.Id> limitToPsId,
       boolean fillAccountLoader,
-      Supplier<I> changeInfoSupplier)
-      throws OrmException {
+      Supplier<I> changeInfoSupplier) {
     try {
       if (fillAccountLoader) {
         accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
@@ -373,19 +376,18 @@
       return toChangeInfo(cd, limitToPsId, changeInfoSupplier);
     } catch (PatchListNotAvailableException
         | GpgException
-        | OrmException
         | IOException
         | PermissionBackendException
         | RuntimeException e) {
       if (!has(CHECK)) {
-        Throwables.throwIfInstanceOf(e, OrmException.class);
-        throw new OrmException(e);
+        Throwables.throwIfInstanceOf(e, StorageException.class);
+        throw new StorageException(e);
       }
       return checkOnly(cd, changeInfoSupplier);
     }
   }
 
-  private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
+  private void ensureLoaded(Iterable<ChangeData> all) {
     if (lazyLoad) {
       ChangeData.ensureChangeLoaded(all);
       if (has(ALL_REVISIONS)) {
@@ -420,7 +422,7 @@
         try {
           ensureLoaded(Collections.singleton(cd));
           changeInfos.add(format(cd, Optional.empty(), false, ChangeInfo::new));
-        } catch (OrmException | RuntimeException e) {
+        } catch (RuntimeException e) {
           logger.atWarning().withCause(e).log(
               "Omitting corrupt change %s from results", cd.getId());
         }
@@ -433,7 +435,7 @@
     ChangeNotes notes;
     try {
       notes = cd.notes();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       String msg = "Error loading change";
       logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
       I info = changeInfoSupplier.get();
@@ -472,8 +474,7 @@
 
   private <I extends ChangeInfo> I toChangeInfo(
       ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
-      throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
-          IOException {
+      throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
       return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier);
     }
@@ -481,8 +482,7 @@
 
   private <I extends ChangeInfo> I toChangeInfoImpl(
       ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
-      throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
-          IOException {
+      throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     I out = changeInfoSupplier.get();
     CurrentUser user = userProvider.get();
 
@@ -504,12 +504,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)) {
@@ -541,7 +541,7 @@
       }
     }
 
-    if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
+    if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
       out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
     }
 
@@ -554,7 +554,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();
       }
@@ -609,8 +609,7 @@
     if (has(TRACKING_IDS)) {
       ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
       out.trackingIds =
-          set.entries()
-              .stream()
+          set.entries().stream()
               .map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
               .collect(toList());
     }
@@ -634,7 +633,7 @@
     return reviewerMap;
   }
 
-  private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) throws OrmException {
+  private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) {
     List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
     List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
     for (ReviewerStatusUpdate c : reviewerUpdates) {
@@ -652,7 +651,7 @@
     return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
   }
 
-  private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException {
+  private void setSubmitter(ChangeData cd, ChangeInfo out) {
     Optional<PatchSetApproval> s = cd.getSubmitApproval();
     if (!s.isPresent()) {
       return;
@@ -661,7 +660,7 @@
     out.submitter = accountLoader.get(s.get().getAccountId());
   }
 
-  private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException {
+  private Collection<ChangeMessageInfo> messages(ChangeData cd) {
     List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
     if (messages.isEmpty()) {
       return Collections.emptyList();
@@ -675,7 +674,7 @@
   }
 
   private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
-      throws PermissionBackendException, OrmException {
+      throws PermissionBackendException {
     // Although this is called removableReviewers, this method also determines
     // which CCs are removable.
     //
@@ -750,23 +749,21 @@
   }
 
   private Collection<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) {
-    return accounts
-        .stream()
+    return accounts.stream()
         .map(accountLoader::get)
         .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
         .collect(toList());
   }
 
   private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
-    return addresses
-        .stream()
+    return addresses.stream()
         .map(a -> new AccountInfo(a.getName(), a.getEmail()))
         .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
         .collect(toList());
   }
 
-  private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
-      throws OrmException {
+  private Map<PatchSet.Id, PatchSet> loadPatchSets(
+      ChangeData cd, Optional<PatchSet.Id> limitToPsId) {
     Collection<PatchSet> src;
     if (has(ALL_REVISIONS) || has(MESSAGES)) {
       src = cd.patchSets();
@@ -775,12 +772,12 @@
       if (limitToPsId.isPresent()) {
         ps = cd.patchSet(limitToPsId.get());
         if (ps == null) {
-          throw new OrmException("missing patch set " + limitToPsId.get());
+          throw new StorageException("missing patch set " + limitToPsId.get());
         }
       } else {
         ps = cd.currentPatchSet();
         if (ps == null) {
-          throw new OrmException("missing current patch set for change " + cd.getId());
+          throw new StorageException("missing current patch set for change " + cd.getId());
         }
       }
       src = Collections.singletonList(ps);
@@ -797,8 +794,7 @@
    *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
    *     lazyload}.
    */
-  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
-      throws OrmException {
+  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) {
     PermissionBackend.WithUser withUser = permissionBackend.user(user);
     return lazyLoad
         ? withUser.change(cd)
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 27def1c..4c68f2d 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Change;
@@ -38,7 +39,6 @@
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.name.Named;
@@ -79,7 +79,6 @@
     };
   }
 
-  @VisibleForTesting
   public static class NoCache implements ChangeKindCache {
     private final boolean useRecursiveMerge;
     private final ChangeData.Factory changeDataFactory;
@@ -393,7 +392,7 @@
                   ObjectId.fromString(priorPs.getRevision().get()),
                   ObjectId.fromString(patch.getRevision().get()));
         }
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         // Do nothing; assume we have a complex change
         logger.atWarning().withCause(e).log(
             "Unable to get change kind for patchSet %s of change %s",
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 3ec61ef..98b728f 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -21,6 +21,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -39,7 +40,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
@@ -159,7 +159,7 @@
       // 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(notes).all());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       // This ETag will be invalidated if it loads next time.
     }
 
@@ -175,7 +175,7 @@
     ObjectId noteId;
     try {
       noteId = notes.loadRevision();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       noteId = null; // This ETag will be invalidated if it loads next time.
     }
     hashObjectId(h, noteId, buf);
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7a553e3..80b7190 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -30,8 +30,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ProblemInfo.Status;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -56,7 +56,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -65,6 +64,7 @@
 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;
@@ -232,7 +232,7 @@
         problem(
             String.format("Current patch set %d not found", change().currentPatchSetId().get()));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       error("Failed to look up current patch set", e);
     }
   }
@@ -256,7 +256,7 @@
     try {
       // Iterate in descending order.
       all = PS_ID_ORDER.sortedCopy(psUtil.byChange(notes));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       return error("Failed to look up patch sets", e);
     }
     patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build();
@@ -361,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,7 +425,7 @@
           if (!c.getDest().equals(change().getDest())) {
             continue;
           }
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           warn(e);
           // Include this patch set; should cause an error below, which is good.
         }
@@ -459,8 +470,7 @@
               String.format(
                   "Multiple patch sets for expected merged commit %s: %s",
                   commit.name(),
-                  thisCommitPsIds
-                      .stream()
+                  thisCommitPsIds.stream()
                       .sorted(comparing(PatchSet.Id::get))
                       .collect(toImmutableList())));
           break;
@@ -532,12 +542,12 @@
           }
         }
 
+        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));
@@ -546,7 +556,7 @@
       notes = notesFactory.createChecked(inserter.getChange());
       insertPatchSetProblem.status = Status.FIXED;
       insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get();
-    } catch (OrmException | IOException | UpdateException | RestApiException e) {
+    } catch (StorageException | IOException | UpdateException | RestApiException e) {
       warn(e);
       for (ProblemInfo pi : currProblems) {
         pi.status = Status.FIX_FAILED;
@@ -564,7 +574,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       ctx.getChange().setStatus(Change.Status.MERGED);
       ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
       p.status = Status.FIXED;
@@ -662,10 +672,9 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchSetInfoNotAvailableException {
+    public boolean updateChange(ChangeContext ctx) throws PatchSetInfoNotAvailableException {
       // Delete dangling key references.
-      accountPatchReviewStore.run(s -> s.clearReviewed(psId), OrmException.class);
+      accountPatchReviewStore.run(s -> s.clearReviewed(psId));
 
       // For NoteDb setting the state to deleted is sufficient to filter everything out.
       ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
@@ -696,7 +705,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
+        throws PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
       if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
         return false;
       }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
similarity index 84%
rename from java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
rename to java/com/google/gerrit/server/change/DeleteChangeOp.java
index 80bdd1a..63baab7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -21,14 +21,11 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.extensions.events.ChangeDeleted;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -67,14 +64,13 @@
   // 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 {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
     Collection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes());
 
     ensureDeletable(ctx, id, patchSets);
     // Cleaning up is only possible as long as the change and its elements are
     // still part of the database.
-    cleanUpReferences(ctx, id, patchSets);
+    cleanUpReferences(id, patchSets);
 
     ctx.deleteChange();
     changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
@@ -83,8 +79,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) {
@@ -108,15 +103,13 @@
     return revWalk.isMergedInto(revWalk.parseCommit(objectId), revWalk.parseCommit(destId.get()));
   }
 
-  private void cleanUpReferences(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets)
-      throws OrmException, NoSuchChangeException {
+  private void cleanUpReferences(Change.Id id, Collection<PatchSet> patchSets) throws IOException {
     for (PatchSet ps : patchSets) {
-      accountPatchReviewStore.run(s -> s.clearReviewed(ps.getId()), OrmException.class);
+      accountPatchReviewStore.run(s -> s.clearReviewed(ps.getId()));
     }
 
-    // Non-atomic operation on Accounts table; not much we can do to make it
-    // atomic.
-    starredChangesUtil.unstarAll(ctx.getChange().getProject(), id);
+    // Non-atomic operation on All-Users refs; not much we can do to make it atomic.
+    starredChangesUtil.unstarAllForChangeDeletion(id);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
similarity index 71%
rename from java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
rename to java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 3231d16..4b5572b 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -12,22 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.restapi.change;
+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;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.change.NotifyUtil;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collections;
@@ -36,31 +32,24 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
+    DeleteReviewerByEmailOp create(Address reviewer);
   }
 
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
-  private final NotifyUtil notifyUtil;
   private final Address reviewer;
-  private final DeleteReviewerInput input;
 
   private ChangeMessage changeMessage;
   private Change change;
 
   @Inject
   DeleteReviewerByEmailOp(
-      DeleteReviewerSender.Factory deleteReviewerSenderFactory,
-      NotifyUtil notifyUtil,
-      @Assisted Address reviewer,
-      @Assisted DeleteReviewerInput input) {
+      DeleteReviewerSender.Factory deleteReviewerSenderFactory, @Assisted Address reviewer) {
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
-    this.notifyUtil = notifyUtil;
     this.reviewer = reviewer;
-    this.input = input;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException {
+  public boolean updateChange(ChangeContext ctx) {
     change = ctx.getChange();
     PatchSet.Id psId = ctx.getChange().currentPatchSetId();
     String msg = "Removed reviewer " + reviewer;
@@ -79,24 +68,17 @@
 
   @Override
   public void postUpdate(Context ctx) {
-    if (input.notify == null) {
-      if (change.isWorkInProgress()) {
-        input.notify = NotifyHandling.NONE;
-      } else {
-        input.notify = NotifyHandling.ALL;
-      }
-    }
-    if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
-      return;
-    }
     try {
+      NotifyResolver.Result notify = ctx.getNotify(change.getId());
+      if (!notify.shouldNotify()) {
+        return;
+      }
       DeleteReviewerSender cm =
           deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
       cm.setFrom(ctx.getAccountId());
       cm.addReviewersByEmail(Collections.singleton(reviewer));
       cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
-      cm.setNotify(input.notify);
-      cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+      cm.setNotify(notify);
       cm.send();
     } catch (Exception err) {
       logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
similarity index 84%
rename from java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
rename to java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 0cb4816..29458a8 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.change;
 
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,13 +28,12 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 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.extensions.events.ReviewerDeleted;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -68,7 +67,6 @@
   private final ReviewerDeleted reviewerDeleted;
   private final Provider<IdentifiedUser> user;
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
-  private final NotifyUtil notifyUtil;
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
 
@@ -90,7 +88,6 @@
       ReviewerDeleted reviewerDeleted,
       Provider<IdentifiedUser> user,
       DeleteReviewerSender.Factory deleteReviewerSenderFactory,
-      NotifyUtil notifyUtil,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
       @Assisted AccountState reviewerAccount,
@@ -102,7 +99,6 @@
     this.reviewerDeleted = reviewerDeleted;
     this.user = user;
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
-    this.notifyUtil = notifyUtil;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
     this.reviewer = reviewerAccount;
@@ -111,8 +107,7 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, ResourceNotFoundException, OrmException, PermissionBackendException,
-          IOException {
+      throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException {
     Account.Id reviewerId = reviewer.getAccount().getId();
     // Check of removing this reviewer (even if there is no vote processed by the loop below) is OK
     removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), reviewerId);
@@ -169,15 +164,21 @@
 
   @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,
@@ -187,12 +188,11 @@
         changeMessage.getMessage(),
         newApprovals,
         oldApprovals,
-        input.notify,
+        notify.handling(),
         ctx.getWhen());
   }
 
-  private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
-      throws OrmException {
+  private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
     Iterable<PatchSetApproval> approvals;
     approvals = approvalsUtil.byChange(ctx.getNotes()).values();
     return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
@@ -206,22 +206,18 @@
   }
 
   private void emailReviewers(
-      Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
+      NameKey projectName, Change change, ChangeMessage changeMessage, NotifyResolver.Result notify)
+      throws EmailException {
     Account.Id userId = user.get().getAccountId();
     if (userId.equals(reviewer.getAccount().getId())) {
       // The user knows they removed themselves, don't bother emailing them.
       return;
     }
-    try {
-      DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
-      cm.setFrom(userId);
-      cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
-      cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
-      cm.setNotify(input.notify);
-      cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
-      cm.send();
-    } catch (Exception err) {
-      logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
-    }
+    DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
+    cm.setFrom(userId);
+    cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
+    cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+    cm.setNotify(notify);
+    cm.send();
   }
 }
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 7e063bc..8353501 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -16,12 +16,8 @@
 
 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;
@@ -48,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.
@@ -63,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,
@@ -79,8 +73,7 @@
   private final CommentSender.Factory commentSenderFactory;
   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;
@@ -95,8 +88,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
       ThreadLocalRequestContext requestContext,
-      @Assisted NotifyHandling notify,
-      @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
+      @Assisted NotifyResolver.Result notify,
       @Assisted ChangeNotes notes,
       @Assisted PatchSet patchSet,
       @Assisted IdentifiedUser user,
@@ -109,7 +101,6 @@
     this.commentSenderFactory = commentSenderFactory;
     this.requestContext = requestContext;
     this.notify = notify;
-    this.accountsToNotify = accountsToNotify;
     this.notes = notes;
     this.patchSet = patchSet;
     this.user = user;
@@ -136,7 +127,6 @@
       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());
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index 62e9454..09ca258 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -173,8 +173,7 @@
    */
   private static ImmutableSortedSet<String> getMatchingRefNames(
       Set<String> matchingRefs, Collection<Ref> allRefs) {
-    return allRefs
-        .stream()
+    return allRefs.stream()
         .map(Ref::getName)
         .filter(matchingRefs::contains)
         .map(Repository::shortenRefName)
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 11442d2..6fde5a5 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -41,10 +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.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;
@@ -52,7 +51,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.sql.Timestamp;
@@ -101,14 +99,14 @@
    */
   Map<String, LabelInfo> labelsFor(
       AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     if (!standard && !detailed) {
       return null;
     }
 
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelWithStatus> withStatus =
-        cd.change().getStatus() == Change.Status.MERGED
+        cd.change().isMerged()
             ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
             : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
     return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
@@ -116,8 +114,8 @@
 
   /** Returns all labels that the provided user has permission to vote on. */
   Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
-      throws OrmException, PermissionBackendException {
-    boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
+      throws PermissionBackendException {
+    boolean isMerged = cd.change().isMerged();
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelType> toCheck = new HashMap<>();
     for (SubmitRecord rec : submitRecords(cd)) {
@@ -195,7 +193,7 @@
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     Map<String, LabelWithStatus> labels = initLabels(accountLoader, cd, labelTypes, standard);
     if (detailed) {
       setAllApprovals(accountLoader, cd, labels);
@@ -253,8 +251,7 @@
     }
   }
 
-  private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd)
-      throws OrmException {
+  private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd) {
     Map<String, Short> result = new HashMap<>();
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
@@ -274,7 +271,7 @@
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     Set<Account.Id> allUsers = new HashSet<>();
     if (detailed) {
       // Users expect to see all reviewers on closed changes, even if they
@@ -287,7 +284,8 @@
     }
 
     Set<String> labelNames = new HashSet<>();
-    SetMultimap<Id, PatchSetApproval> current = MultimapBuilder.hashKeys().hashSetValues().build();
+    SetMultimap<Account.Id, PatchSetApproval> current =
+        MultimapBuilder.hashKeys().hashSetValues().build();
     for (PatchSetApproval a : cd.currentApprovals()) {
       allUsers.add(a.getAccountId());
       LabelType type = labelTypes.byLabel(a.getLabelId());
@@ -320,9 +318,7 @@
     }
 
     if (detailed) {
-      labels
-          .entrySet()
-          .stream()
+      labels.entrySet().stream()
           .filter(e -> labelTypes.byLabel(e.getKey()) != null)
           .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue()));
     }
@@ -433,21 +429,22 @@
 
   private void setAllApprovals(
       AccountLoader accountLoader, ChangeData cd, Map<String, LabelWithStatus> labels)
-      throws OrmException, PermissionBackendException {
-    Change.Status status = cd.change().getStatus();
+      throws PermissionBackendException {
     checkState(
-        status != Change.Status.MERGED, "should not call setAllApprovals on %s change", status);
+        !cd.change().isMerged(),
+        "should not call setAllApprovals on %s change",
+        ChangeUtil.status(cd.change()));
 
     // Include a user in the output for this label if either:
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
-    Set<Id> allUsers = new HashSet<>();
+    Set<Account.Id> allUsers = new HashSet<>();
     allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
     for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
     }
 
-    Table<Id, String, PatchSetApproval> current =
+    Table<Account.Id, String, PatchSetApproval> current =
         HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
     for (PatchSetApproval psa : cd.currentApprovals()) {
       current.put(psa.getAccountId(), psa.getLabel(), psa);
@@ -500,8 +497,7 @@
    *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
    *     lazyload}.
    */
-  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
-      throws OrmException {
+  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd) {
     PermissionBackend.WithUser withUser = permissionBackend.absentUser(user);
     return lazyLoad
         ? withUser.change(cd)
@@ -518,9 +514,7 @@
         Maps.newHashMapWithExpectedSize(permittedLabels.size());
     for (String label : permittedLabels.keySet()) {
       List<Integer> permittedVotingRange =
-          permittedLabels
-              .get(label)
-              .stream()
+          permittedLabels.get(label).stream()
               .map(this::parseRangeValue)
               .filter(java.util.Objects::nonNull)
               .sorted()
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 1ac558b..d408519 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -207,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..724ef48
--- /dev/null
+++ b/java/com/google/gerrit/server/change/NotifyResolver.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class NotifyResolver {
+  @AutoValue
+  public abstract static class Result {
+    public static Result none() {
+      return create(NotifyHandling.NONE);
+    }
+
+    public static Result all() {
+      return create(NotifyHandling.ALL);
+    }
+
+    public static Result create(NotifyHandling notifyHandling) {
+      return create(notifyHandling, ImmutableSetMultimap.of());
+    }
+
+    public static Result create(
+        NotifyHandling handling, ImmutableSetMultimap<RecipientType, Account.Id> recipients) {
+      return new AutoValue_NotifyResolver_Result(handling, recipients);
+    }
+
+    public abstract NotifyHandling handling();
+
+    // 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, IOException, ConfigInvalidException {
+    requireNonNull(handling);
+    ImmutableSetMultimap.Builder<RecipientType, Account.Id> b = ImmutableSetMultimap.builder();
+    if (notifyDetails != null) {
+      for (Map.Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
+        b.putAll(e.getKey(), find(e.getValue().accounts));
+      }
+    }
+    return Result.create(handling, b.build());
+  }
+
+  private ImmutableList<Account.Id> find(@Nullable List<String> inputs)
+      throws BadRequestException, IOException, ConfigInvalidException {
+    if (inputs == null || inputs.isEmpty()) {
+      return ImmutableList.of();
+    }
+    ImmutableList.Builder<Account.Id> r = ImmutableList.builder();
+    List<String> problems = new ArrayList<>(inputs.size());
+    for (String nameOrEmail : inputs) {
+      try {
+        r.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
+      } catch (UnprocessableEntityException e) {
+        problems.add(e.getMessage());
+      }
+    }
+
+    if (!problems.isEmpty()) {
+      throw new BadRequestException(
+          "Some accounts that should be notified could not be resolved: "
+              + problems.stream().collect(joining("\n")));
+    }
+
+    return r.build();
+  }
+}
diff --git a/java/com/google/gerrit/server/change/NotifyUtil.java b/java/com/google/gerrit/server/change/NotifyUtil.java
deleted file mode 100644
index c29faee..0000000
--- a/java/com/google/gerrit/server/change/NotifyUtil.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.NotifyInfo;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-@Singleton
-public class NotifyUtil {
-  private final AccountResolver accountResolver;
-
-  @Inject
-  NotifyUtil(AccountResolver accountResolver) {
-    this.accountResolver = accountResolver;
-  }
-
-  public static boolean shouldNotify(
-      NotifyHandling notify, @Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
-    if (!isNullOrEmpty(notifyDetails)) {
-      return true;
-    }
-
-    return notify.compareTo(NotifyHandling.NONE) > 0;
-  }
-
-  private static boolean isNullOrEmpty(@Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
-    if (notifyDetails == null || notifyDetails.isEmpty()) {
-      return true;
-    }
-
-    for (NotifyInfo notifyInfo : notifyDetails.values()) {
-      if (!isEmpty(notifyInfo)) {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  private static boolean isEmpty(NotifyInfo notifyInfo) {
-    return notifyInfo.accounts == null || notifyInfo.accounts.isEmpty();
-  }
-
-  public ListMultimap<RecipientType, Account.Id> resolveAccounts(
-      @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
-      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
-    if (isNullOrEmpty(notifyDetails)) {
-      return ImmutableListMultimap.of();
-    }
-
-    ListMultimap<RecipientType, Account.Id> m = null;
-    for (Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
-      List<String> accounts = e.getValue().accounts;
-      if (accounts != null) {
-        if (m == null) {
-          m = MultimapBuilder.hashKeys().arrayListValues().build();
-        }
-        m.putAll(e.getKey(), find(accounts));
-      }
-    }
-
-    return m != null ? m : ImmutableListMultimap.of();
-  }
-
-  private List<Account.Id> find(List<String> nameOrEmails)
-      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
-    List<String> missing = new ArrayList<>(nameOrEmails.size());
-    List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
-    for (String nameOrEmail : nameOrEmails) {
-      Account a = accountResolver.find(nameOrEmail);
-      if (a != null) {
-        r.add(a.getId());
-      } else {
-        missing.add(nameOrEmail);
-      }
-    }
-
-    if (!missing.isEmpty()) {
-      throw new BadRequestException(
-          "The following accounts that should be notified could not be resolved: "
-              + missing.stream().distinct().sorted().collect(joining(", ")));
-    }
-
-    return r;
-  }
-}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index ec11c1b..d3649f6 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -19,14 +19,10 @@
 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;
@@ -53,7 +49,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -95,9 +90,8 @@
   private boolean checkAddPatchSetPermission = true;
   private List<String> groups = Collections.emptyList();
   private boolean fireRevisionCreated = true;
-  private NotifyHandling notify = NotifyHandling.ALL;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
   private boolean allowClosed;
+  private boolean sendEmail = true;
 
   // Fields set during some phase of BatchUpdate.Op.
   private Change change;
@@ -170,22 +164,16 @@
     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 setSendEmail(boolean sendEmail) {
+    this.sendEmail = sendEmail;
+    return this;
+  }
+
   public Change getChange() {
     checkState(change != null, "getChange() only valid after executing update");
     return change;
@@ -198,20 +186,18 @@
 
   @Override
   public void updateRepo(RepoContext ctx)
-      throws AuthException, ResourceConflictException, IOException, OrmException,
-          PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     validate(ctx);
     ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws ResourceConflictException, OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
     change = ctx.getChange();
     ChangeUpdate update = ctx.getUpdate(psId);
     update.setSubjectForCommit("Create patch set " + psId.get());
 
-    if (!change.getStatus().isOpen() && !allowClosed) {
+    if (!change.isNew() && !allowClosed) {
       throw new ResourceConflictException(
           String.format(
               "Cannot create new patch set of change %s because it is %s",
@@ -229,7 +215,7 @@
         psUtil.insert(
             ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
 
-    if (notify != NotifyHandling.NONE) {
+    if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
       oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
     }
 
@@ -257,8 +243,10 @@
   }
 
   @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());
@@ -267,7 +255,6 @@
         cm.addReviewers(oldReviewers.byState(REVIEWER));
         cm.addExtraCC(oldReviewers.byState(CC));
         cm.setNotify(notify);
-        cm.setAccountsToNotify(accountsToNotify);
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
@@ -281,8 +268,7 @@
   }
 
   private void validate(RepoContext ctx)
-      throws AuthException, ResourceConflictException, IOException, PermissionBackendException,
-          OrmException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     // Not allowed to create a new patch set if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(origNotes);
 
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/query/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
similarity index 82%
rename from java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
rename to java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
index a795025..08d6ce7 100644
--- a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
+++ b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.query.change;
+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 {
diff --git a/java/com/google/gerrit/server/change/PureRevert.java b/java/com/google/gerrit/server/change/PureRevert.java
index 0135683..0859634 100644
--- a/java/com/google/gerrit/server/change/PureRevert.java
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -14,109 +14,48 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
+import java.util.Optional;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
+/** Can check if a change is a pure revert (= a revert with no further modifications). */
 @Singleton
 public class PureRevert {
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final GitRepositoryManager repoManager;
-  private final ProjectCache projectCache;
-  private final ChangeNotes.Factory notesFactory;
-  private final PatchSetUtil psUtil;
+  private final PureRevertCache pureRevertCache;
 
   @Inject
-  PureRevert(
-      MergeUtil.Factory mergeUtilFactory,
-      GitRepositoryManager repoManager,
-      ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory,
-      PatchSetUtil psUtil) {
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.repoManager = repoManager;
-    this.projectCache = projectCache;
-    this.notesFactory = notesFactory;
-    this.psUtil = psUtil;
+  PureRevert(PureRevertCache pureRevertCache) {
+    this.pureRevertCache = pureRevertCache;
   }
 
-  public PureRevertInfo get(ChangeNotes notes, @Nullable String claimedOriginal)
-      throws OrmException, IOException, BadRequestException, ResourceConflictException {
-    PatchSet currentPatchSet = psUtil.current(notes);
+  public boolean get(ChangeNotes notes, Optional<String> claimedOriginal)
+      throws IOException, BadRequestException, ResourceConflictException {
+    PatchSet currentPatchSet = notes.getCurrentPatchSet();
     if (currentPatchSet == null) {
       throw new ResourceConflictException("current revision is missing");
     }
-
-    if (claimedOriginal == null) {
-      if (notes.getChange().getRevertOf() == null) {
-        throw new BadRequestException("no ID was provided and change isn't a revert");
-      }
-      PatchSet ps =
-          psUtil.current(
-              notesFactory.createChecked(notes.getProjectName(), notes.getChange().getRevertOf()));
-      claimedOriginal = ps.getRevision().get();
+    if (!claimedOriginal.isPresent()) {
+      return pureRevertCache.isPureRevert(notes);
     }
 
-    try (Repository repo = repoManager.openRepository(notes.getProjectName());
-        ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit claimedOriginalCommit;
-      try {
-        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
-      } catch (InvalidObjectIdException | MissingObjectException e) {
-        throw new BadRequestException("invalid object ID");
-      }
-      if (claimedOriginalCommit.getParentCount() == 0) {
-        throw new BadRequestException("can't check against initial commit");
-      }
-      RevCommit claimedRevertCommit =
-          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
-      if (claimedRevertCommit.getParentCount() == 0) {
-        throw new BadRequestException("claimed revert has no parents");
-      }
-      // Rebase claimed revert onto claimed original
-      ThreeWayMerger merger =
-          mergeUtilFactory
-              .create(projectCache.checkedGet(notes.getProjectName()))
-              .newThreeWayMerger(oi, repo.getConfig());
-      merger.setBase(claimedRevertCommit.getParent(0));
-      boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
-      if (!success || merger.getResultTreeId() == null) {
-        // Merge conflict during rebase
-        return new PureRevertInfo(false);
-      }
-
-      // Any differences between claimed original's parent and the rebase result indicate that the
-      // claimedRevert is not a pure revert but made content changes
-      try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
-        df.setReader(oi.newReader(), repo.getConfig());
-        List<DiffEntry> entries =
-            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
-        return new PureRevertInfo(entries.isEmpty());
-      }
+    ObjectId claimedOriginalObjectId;
+    try {
+      claimedOriginalObjectId = ObjectId.fromString(claimedOriginal.get());
+    } catch (InvalidObjectIdException e) {
+      throw new BadRequestException("invalid object ID");
     }
+
+    return pureRevertCache.isPureRevert(
+        notes.getProjectName(),
+        ObjectId.fromString(notes.getCurrentPatchSet().getRevision().get()),
+        claimedOriginalObjectId);
   }
 }
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index a64900b..fccda7c 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -16,11 +16,9 @@
 
 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;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeUtil;
@@ -39,7 +37,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -73,6 +70,7 @@
   private boolean forceContentMerge;
   private boolean detailedCommitMessage;
   private boolean postMessage = true;
+  private boolean sendEmail = true;
   private boolean matchAuthorToCommitterDate = false;
 
   private RevCommit rebasedCommit;
@@ -137,6 +135,11 @@
     return this;
   }
 
+  public RebaseChangeOp setSendEmail(boolean sendEmail) {
+    this.sendEmail = sendEmail;
+    return this;
+  }
+
   public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
     this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
     return this;
@@ -145,7 +148,7 @@
   @Override
   public void updateRepo(RepoContext ctx)
       throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
-          OrmException, NoSuchChangeException, PermissionBackendException {
+          NoSuchChangeException, PermissionBackendException {
     // Ok that originalPatchSet was not read in a transaction, since we just
     // need its revision.
     RevId oldRev = originalPatchSet.getRevision();
@@ -183,10 +186,10 @@
         patchSetInserterFactory
             .create(notes, rebasedPatchSetId, rebasedCommit)
             .setDescription("Rebase")
-            .setNotify(NotifyHandling.NONE)
             .setFireRevisionCreated(fireRevisionCreated)
             .setCheckAddPatchSetPermission(checkAddPatchSetPermission)
-            .setValidate(validate);
+            .setValidate(validate)
+            .setSendEmail(sendEmail);
     if (postMessage) {
       patchSetInserter.setMessage(
           "Patch Set "
@@ -196,8 +199,8 @@
               + " was rebased");
     }
 
-    if (base != null && base.notes().getChange().getStatus() != Change.Status.MERGED) {
-      if (base.notes().getChange().getStatus() != Change.Status.MERGED) {
+    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 {
@@ -209,15 +212,14 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx)
-      throws ResourceConflictException, OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
     boolean ret = patchSetInserter.updateChange(ctx);
     rebasedPatchSet = patchSetInserter.getPatchSet();
     return ret;
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     patchSetInserter.postUpdate(ctx);
   }
 
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 6cb61c1..a4cf5ba 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -17,19 +17,18 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -63,7 +62,7 @@
       return true;
     } catch (RestApiException e) {
       return false;
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atWarning().withCause(e).log(
           "Error checking if patch set %s on %s can be rebased", patchSet.getId(), dest);
       return false;
@@ -84,7 +83,7 @@
     public abstract PatchSet patchSet();
   }
 
-  public Base parseBase(RevisionResource rsrc, String base) throws OrmException {
+  public Base parseBase(RevisionResource rsrc, String base) {
     // Try parsing the base as a ref string.
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
@@ -120,7 +119,7 @@
     return ret;
   }
 
-  private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) throws OrmException {
+  private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) {
     if (rsrc.getChange().getId().equals(id)) {
       return rsrc.getNotes();
     }
@@ -140,11 +139,10 @@
    * @return the commit onto which the patch set should be rebased.
    * @throws RestApiException if rebase is not possible.
    * @throws IOException if accessing the repository fails.
-   * @throws OrmException if accessing the database fails.
    */
   public ObjectId findBaseRevision(
       PatchSet patchSet, Branch.NameKey destBranch, Repository git, RevWalk rw)
-      throws RestApiException, IOException, OrmException {
+      throws RestApiException, IOException {
     String baseRev = null;
     RevCommit commit = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
 
@@ -164,12 +162,12 @@
           continue;
         }
         Change depChange = cd.change();
-        if (depChange.getStatus() == Status.ABANDONED) {
+        if (depChange.isAbandoned()) {
           throw new ResourceConflictException(
               "Cannot rebase a change with an abandoned parent: " + depChange.getKey());
         }
 
-        if (depChange.getStatus().isOpen()) {
+        if (depChange.isNew()) {
           if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
             throw new ResourceConflictException(
                 "Change is already based on the latest patch set of the dependent change.");
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index f318001..a6ad559 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -24,9 +24,7 @@
 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;
@@ -36,12 +34,10 @@
 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;
@@ -72,7 +68,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -151,7 +146,6 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final Config cfg;
   private final ReviewerJson json;
-  private final NotifyUtil notifyUtil;
   private final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousProvider;
   private final AddReviewersOp.Factory addReviewersOpFactory;
@@ -166,7 +160,6 @@
       AccountLoader.Factory accountLoaderFactory,
       @GerritServerConfig Config cfg,
       ReviewerJson json,
-      NotifyUtil notifyUtil,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousProvider,
       AddReviewersOp.Factory addReviewersOpFactory,
@@ -178,7 +171,6 @@
     this.accountLoaderFactory = accountLoaderFactory;
     this.cfg = cfg;
     this.json = json;
-    this.notifyUtil = notifyUtil;
     this.projectCache = projectCache;
     this.anonymousProvider = anonymousProvider;
     this.addReviewersOpFactory = addReviewersOpFactory;
@@ -195,47 +187,47 @@
    * @return handle describing the addition operation. If the {@code op} field is present, this
    *     operation may be added to a {@code BatchUpdate}. Otherwise, the {@code error} field
    *     contains information about an error that occurred
-   * @throws OrmException
    * @throws IOException
    * @throws PermissionBackendException
    * @throws ConfigInvalidException
    */
   public ReviewerAddition prepare(
       ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     requireNonNull(input.reviewer);
-    ListMultimap<RecipientType, Account.Id> accountsToNotify;
-    try {
-      accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
-    } catch (BadRequestException e) {
-      return fail(input, FailureType.OTHER, e.getMessage());
-    }
     boolean confirmed = input.confirmed();
     boolean allowByEmail =
         projectCache
             .checkedGet(notes.getProjectName())
             .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
 
-    ReviewerAddition byAccountId =
-        addByAccountId(input, notes, user, accountsToNotify, allowGroup, allowByEmail);
+    ReviewerAddition byAccountId = addByAccountId(input, notes, user);
 
     ReviewerAddition wholeGroup = null;
-    if (byAccountId == null || !byAccountId.exactMatchFound) {
-      wholeGroup =
-          addWholeGroup(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(input, notes, user, accountsToNotify);
+    return addByEmail(input, notes, user);
   }
 
   public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
@@ -245,57 +237,30 @@
         revision.getUser(),
         ImmutableSet.of(user.getAccountId()),
         null,
-        ImmutableListMultimap.of(),
         true);
   }
 
   @Nullable
   private ReviewerAddition addByAccountId(
-      AddReviewerInput input,
-      ChangeNotes notes,
-      CurrentUser user,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean allowGroup,
-      boolean allowByEmail)
-      throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
+      AddReviewerInput input, ChangeNotes notes, CurrentUser user)
+      throws PermissionBackendException, IOException, ConfigInvalidException {
     IdentifiedUser reviewerUser;
     boolean exactMatchFound = false;
     try {
-      reviewerUser = accountResolver.parse(input.reviewer);
+      reviewerUser = accountResolver.resolve(input.reviewer).asUniqueUser();
       if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
           || input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
         exactMatchFound = true;
       }
-    } catch (UnprocessableEntityException | AuthException e) {
-      // AuthException won't occur since the user is authenticated at this point.
-      if (!allowGroup && !allowByEmail) {
-        // Only return failure if we aren't going to try other interpretations.
-        return fail(
-            input,
-            FailureType.NOT_FOUND,
-            MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
-      }
-      return null;
+    } catch (UnprocessableEntityException e) {
+      // Caller might choose to ignore this NOT_FOUND result if they find another result e.g. by
+      // group, but if not, the error message will be useful.
+      return fail(input, FailureType.NOT_FOUND, e.getMessage());
     }
 
     if (isValidReviewer(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,
@@ -308,7 +273,6 @@
       AddReviewerInput input,
       ChangeNotes notes,
       CurrentUser user,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
       boolean confirmed,
       boolean allowGroup,
       boolean allowByEmail)
@@ -380,15 +344,11 @@
       }
     }
 
-    return new ReviewerAddition(input, notes, user, reviewers, null, accountsToNotify, true);
+    return new ReviewerAddition(input, notes, user, reviewers, null, true);
   }
 
   @Nullable
-  private ReviewerAddition addByEmail(
-      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()).change(notes).check(ChangePermission.READ);
@@ -406,16 +366,11 @@
           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(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
@@ -466,7 +421,6 @@
         CurrentUser caller,
         @Nullable Iterable<Account.Id> reviewers,
         @Nullable Iterable<Address> reviewersByEmail,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify,
         boolean exactMatchFound) {
       checkArgument(
           reviewers != null || reviewersByEmail != null,
@@ -481,9 +435,7 @@
       this.reviewersByEmail =
           reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
       this.caller = caller.asIdentifiedUser();
-      op =
-          addReviewersOpFactory.create(
-              this.reviewers, this.reviewersByEmail, state(), input.notify, accountsToNotify);
+      op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
       this.exactMatchFound = exactMatchFound;
     }
 
@@ -495,7 +447,7 @@
           : ImmutableSet.of();
     }
 
-    public void gatherResults(ChangeData cd) throws OrmException, PermissionBackendException {
+    public void gatherResults(ChangeData cd) throws PermissionBackendException {
       checkState(op != null, "addition did not result in an update op");
       checkState(op.getResult() != null, "op did not return a result");
 
@@ -556,7 +508,7 @@
       CurrentUser user,
       Iterable<? extends AddReviewerInput> inputs,
       boolean allowGroup)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     // Process CC ops before reviewer ops, so a user that appears in both lists ends up as a
     // reviewer; the last call to ChangeUpdate#putReviewer wins. This can happen if the caller
     // specifies the same string twice, or less obviously if they specify multiple groups with
@@ -568,11 +520,17 @@
         Streams.stream(inputs)
             .sorted(
                 comparing(
-                    i -> i.state(), Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
+                    AddReviewerInput::state,
+                    Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
             .collect(toImmutableList());
     List<ReviewerAddition> additions = new ArrayList<>();
     for (AddReviewerInput input : sorted) {
-      additions.add(prepare(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);
   }
@@ -590,8 +548,7 @@
     }
 
     public ImmutableList<ReviewerAddition> getFailures() {
-      return additions
-          .stream()
+      return additions.stream()
           .filter(a -> a.isFailure() && !a.isIgnorableFailure())
           .collect(toImmutableList());
     }
@@ -599,7 +556,7 @@
     // We never call updateRepo on the addition ops, which is only ok because it's a no-op.
 
     public void updateChange(ChangeContext ctx, PatchSet patchSet)
-        throws OrmException, RestApiException, IOException {
+        throws RestApiException, IOException {
       for (ReviewerAddition addition : additions()) {
         addition.op.setPatchSet(patchSet);
         addition.op.updateChange(ctx);
@@ -621,8 +578,7 @@
               a ->
                   checkArgument(
                       a.op != null && a.op.getResult() != null, "missing result on %s", a));
-      return additions()
-          .stream()
+      return additions().stream()
           .map(a -> a.op.getResult())
           .map(func)
           .flatMap(Collection::stream)
@@ -630,8 +586,7 @@
     }
 
     private ImmutableList<ReviewerAddition> additions() {
-      return additions
-          .stream()
+      return additions.stream()
           .filter(
               a -> {
                 if (a.isFailure()) {
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index fd5772d..2742bb9 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collection;
@@ -65,7 +64,7 @@
   }
 
   public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
     AccountLoader loader = accountLoaderFactory.create(true);
     ChangeData cd = null;
@@ -88,13 +87,12 @@
     return infos;
   }
 
-  public List<ReviewerInfo> format(ReviewerResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public List<ReviewerInfo> format(ReviewerResource rsrc) throws PermissionBackendException {
     return format(ImmutableList.of(rsrc));
   }
 
   public ReviewerInfo format(ReviewerInfo out, Account.Id reviewerAccountId, ChangeData cd)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     PatchSet.Id psId = cd.change().currentPatchSetId();
     return format(
         out,
@@ -108,7 +106,7 @@
       Account.Id reviewerAccountId,
       ChangeData cd,
       Iterable<PatchSetApproval> approvals)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     LabelTypes labelTypes = cd.getLabelTypes();
 
     out.approvals = new TreeMap<>(labelTypes.nameComparator());
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index b67028d..aa733cd 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -66,7 +65,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -156,8 +154,7 @@
    * depending on the options provided when constructing this instance.
    */
   public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
@@ -212,10 +209,9 @@
       AccountLoader accountLoader,
       ChangeData cd,
       Map<PatchSet.Id, PatchSet> map,
-      Optional<Id> limitToPsId,
+      Optional<PatchSet.Id> limitToPsId,
       ChangeInfo changeInfo)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
     try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
@@ -240,7 +236,7 @@
   }
 
   private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
-      throws PermissionBackendException, OrmException, IOException {
+      throws PermissionBackendException, IOException {
     Map<String, FetchInfo> r = new LinkedHashMap<>();
     for (Extension<DownloadScheme> e : downloadSchemes) {
       String schemeName = e.getExportName();
@@ -276,8 +272,7 @@
       @Nullable RevWalk rw,
       boolean fillCommit,
       @Nullable ChangeInfo changeInfo)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     Change c = cd.change();
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(c.currentPatchSetId());
@@ -351,14 +346,13 @@
    *     lazyload}.
    */
   private PermissionBackend.ForChange permissionBackendForChange(
-      PermissionBackend.WithUser withUser, ChangeData cd) throws OrmException {
+      PermissionBackend.WithUser withUser, ChangeData cd) {
     return lazyLoad
         ? withUser.change(cd)
         : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
   }
 
-  private boolean isWorldReadable(ChangeData cd)
-      throws OrmException, PermissionBackendException, IOException {
+  private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException, IOException {
     try {
       permissionBackendForChange(permissionBackend.user(anonymous), cd)
           .check(ChangePermission.READ);
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index deb5022..caafe24 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -34,7 +34,7 @@
   public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
       new TypeLiteral<RestView<RevisionResource>>() {};
 
-  public static RevisionResource createNonCachable(ChangeResource change, PatchSet ps) {
+  public static RevisionResource createNonCacheable(ChangeResource change, PatchSet ps) {
     return new RevisionResource(change, ps, Optional.empty(), false);
   }
 
@@ -52,11 +52,11 @@
   }
 
   private RevisionResource(
-      ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cachable) {
+      ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cacheable) {
     this.change = change;
     this.ps = ps;
     this.edit = edit;
-    this.cacheable = cachable;
+    this.cacheable = cacheable;
   }
 
   public boolean isCacheable() {
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index dd24ff6..8d350c3 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.validators.AssigneeValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -74,7 +73,7 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, RestApiException {
+  public boolean updateChange(ChangeContext ctx) throws RestApiException {
     change = ctx.getChange();
     if (newAssignee.getAccountId().equals(change.getAssignee())) {
       return false;
@@ -117,7 +116,7 @@
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     try {
       SetAssigneeSender cm =
           setAssigneeSenderFactory.create(
diff --git a/java/com/google/gerrit/server/change/SetHashtagsOp.java b/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 4f73053..abc4eee 100644
--- a/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -82,8 +81,7 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, BadRequestException, MethodNotAllowedException, OrmException,
-          IOException {
+      throws AuthException, BadRequestException, MethodNotAllowedException, IOException {
     if (input == null || (input.add == null && input.remove == null)) {
       updatedHashtags = ImmutableSortedSet.of();
       return false;
@@ -146,7 +144,7 @@
   }
 
   @Override
-  public void postUpdate(Context ctx) throws OrmException {
+  public void postUpdate(Context ctx) {
     if (updated() && fireEvent) {
       hashtagsEdited.fire(
           change, ctx.getAccount(), updatedHashtags, toAdd, toRemove, ctx.getWhen());
diff --git a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java b/java/com/google/gerrit/server/change/SetPrivateOp.java
similarity index 78%
rename from java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
rename to java/com/google/gerrit/server/change/SetPrivateOp.java
index 04c94be..1600fd5 100644
--- a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -12,14 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.change;
 
 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;
@@ -27,7 +30,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -43,35 +45,47 @@
   }
 
   public interface Factory {
-    SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input);
+    SetPrivateOp create(boolean isPrivate, @Nullable Input input);
   }
 
-  private final ChangeMessagesUtil cmUtil;
-  private final PatchSetUtil psUtil;
-  private final boolean isPrivate;
-  private final Input input;
   private final PrivateStateChanged privateStateChanged;
+  private final PatchSetUtil psUtil;
+  private final ChangeMessagesUtil cmUtil;
+  private final boolean isPrivate;
+  @Nullable private final Input input;
 
   private Change change;
   private PatchSet ps;
+  private boolean isNoOp;
 
   @Inject
   SetPrivateOp(
       PrivateStateChanged privateStateChanged,
       PatchSetUtil psUtil,
-      @Assisted ChangeMessagesUtil cmUtil,
+      ChangeMessagesUtil cmUtil,
       @Assisted boolean isPrivate,
-      @Assisted Input input) {
-    this.cmUtil = cmUtil;
+      @Assisted @Nullable Input input) {
+    this.privateStateChanged = privateStateChanged;
     this.psUtil = psUtil;
+    this.cmUtil = cmUtil;
     this.isPrivate = isPrivate;
     this.input = input;
-    this.privateStateChanged = privateStateChanged;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException {
+  public boolean updateChange(ChangeContext ctx)
+      throws ResourceConflictException, BadRequestException {
     change = ctx.getChange();
+    if (ctx.getChange().isPrivate() == isPrivate) {
+      // No-op
+      isNoOp = true;
+      return false;
+    }
+
+    if (isPrivate && !change.isNew()) {
+      throw new BadRequestException(
+          String.format("cannot set %s change to private", ChangeUtil.status(change)));
+    }
     ChangeNotes notes = ctx.getNotes();
     ps = psUtil.get(notes, change.currentPatchSetId());
     ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
@@ -84,7 +98,9 @@
 
   @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) {
diff --git a/java/com/google/gerrit/server/change/TestSubmitInput.java b/java/com/google/gerrit/server/change/TestSubmitInput.java
index b681bf8..bb85e66 100644
--- a/java/com/google/gerrit/server/change/TestSubmitInput.java
+++ b/java/com/google/gerrit/server/change/TestSubmitInput.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package com.google.gerrit.server.change;
 
 import com.google.common.annotations.VisibleForTesting;
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 916a62b..5945a0c 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -24,11 +24,11 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayDeque;
@@ -73,7 +73,7 @@
                 }
                 try {
                   return in.get(0).data().change().getProject();
-                } catch (OrmException e) {
+                } catch (StorageException e) {
                   throw new IllegalStateException(e);
                 }
               });
@@ -98,7 +98,7 @@
     return this;
   }
 
-  public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws OrmException, IOException {
+  public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws IOException {
     ListMultimap<Project.NameKey, ChangeData> byProject =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeData cd : in) {
@@ -114,7 +114,7 @@
   }
 
   private List<PatchSetData> sortProject(Project.NameKey project, Collection<ChangeData> in)
-      throws OrmException, IOException {
+      throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.setRetainBody(retainBody);
@@ -217,7 +217,7 @@
   }
 
   private ListMultimap<RevCommit, PatchSetData> byCommit(RevWalk rw, Collection<ChangeData> in)
-      throws OrmException, IOException {
+      throws IOException {
     ListMultimap<RevCommit, PatchSetData> byCommit =
         MultimapBuilder.hashKeys(in.size()).arrayListValues(1).build();
     for (ChangeData cd : in) {
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 1da6d16..f3f1a29 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -14,30 +14,21 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -59,42 +50,14 @@
     WorkInProgressOp create(boolean workInProgress, Input in);
   }
 
-  public static void checkPermissions(
-      PermissionBackend permissionBackend, CurrentUser user, Change change)
-      throws PermissionBackendException, AuthException {
-    if (!user.isIdentifiedUser()) {
-      throw new AuthException("Authentication required");
-    }
-
-    if (change.getOwner().equals(user.asIdentifiedUser().getAccountId())) {
-      return;
-    }
-
-    try {
-      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-      return;
-    } catch (AuthException e) {
-      // Skip.
-    }
-
-    try {
-      permissionBackend
-          .user(user)
-          .project(change.getProject())
-          .check(ProjectPermission.WRITE_CONFIG);
-    } catch (AuthException exp) {
-      throw new AuthException("not allowed to toggle work in progress");
-    }
-  }
-
   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,13 +77,14 @@
     this.stateChanged = stateChanged;
     this.workInProgress = workInProgress;
     this.in = in;
-    notify =
-        MoreObjects.firstNonNull(
-            in.notify, workInProgress ? NotifyHandling.NONE : NotifyHandling.ALL);
+  }
+
+  public void suppressEmail() {
+    this.sendEmail = false;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException {
+  public boolean updateChange(ChangeContext ctx) {
     change = ctx.getChange();
     notes = ctx.getNotes();
     ps = psUtil.get(ctx.getNotes(), change.currentPatchSetId());
@@ -160,13 +124,15 @@
   @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/ChangeCleanupConfig.java b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
index f492247..f5c9fc2 100644
--- a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
+++ b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -34,17 +35,19 @@
           + "\n"
           + "If this change is still wanted it should be restored.";
 
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final Optional<Schedule> schedule;
   private final long abandonAfter;
   private final boolean abandonIfMergeable;
   private final String abandonMessage;
 
   @Inject
-  ChangeCleanupConfig(@GerritServerConfig Config cfg, UrlFormatter urlFormatter) {
+  ChangeCleanupConfig(@GerritServerConfig Config cfg, DynamicItem<UrlFormatter> urlFormatter) {
+    this.urlFormatter = urlFormatter;
     schedule = ScheduleConfig.createSchedule(cfg, SECTION);
     abandonAfter = readAbandonAfter(cfg);
     abandonIfMergeable = cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true);
-    abandonMessage = readAbandonMessage(cfg, urlFormatter);
+    abandonMessage = readAbandonMessage(cfg);
   }
 
   private long readAbandonAfter(Config cfg) {
@@ -53,18 +56,9 @@
     return abandonAfter >= 0 ? abandonAfter : 0;
   }
 
-  private String readAbandonMessage(Config cfg, UrlFormatter urlFormatter) {
+  private String readAbandonMessage(Config cfg) {
     String abandonMessage = cfg.getString(SECTION, null, KEY_ABANDON_MESSAGE);
-    if (Strings.isNullOrEmpty(abandonMessage)) {
-      abandonMessage = DEFAULT_ABANDON_MESSAGE;
-    }
-
-    String docUrl = urlFormatter.getDocUrl("user-change-cleanup.html", "auto-abandon").orElse("");
-    if (!docUrl.isEmpty()) {
-      abandonMessage = abandonMessage.replaceAll("\\$\\{URL\\}", docUrl);
-    }
-
-    return abandonMessage;
+    return Strings.isNullOrEmpty(abandonMessage) ? DEFAULT_ABANDON_MESSAGE : abandonMessage;
   }
 
   public Optional<Schedule> getSchedule() {
@@ -80,6 +74,8 @@
   }
 
   public String getAbandonMessage() {
-    return abandonMessage;
+    String docUrl =
+        urlFormatter.get().getDocUrl("user-change-cleanup.html", "auto-abandon").orElse("");
+    return docUrl.isEmpty() ? abandonMessage : abandonMessage.replace("${URL}", docUrl);
   }
 }
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 600ce10..b37e489 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -97,8 +97,7 @@
   private Multimap<UpdateResult, ConfigUpdateEntry> createUpdate(
       Set<ConfigKey> entries, UpdateResult updateResult) {
     Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
-    entries
-        .stream()
+    entries.stream()
         .filter(this::isValueUpdated)
         .map(e -> new ConfigUpdateEntry(e, getString(e, oldConfig), getString(e, newConfig)))
         .forEach(e -> updates.put(updateResult, e));
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java b/java/com/google/gerrit/server/config/EnableReverseDnsLookup.java
similarity index 94%
rename from java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
rename to java/com/google/gerrit/server/config/EnableReverseDnsLookup.java
index 336edeb..ec57338 100644
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
+++ b/java/com/google/gerrit/server/config/EnableReverseDnsLookup.java
@@ -21,4 +21,4 @@
 
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface DisableReverseDnsLookup {}
+public @interface EnableReverseDnsLookup {}
diff --git a/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java b/java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java
similarity index 71%
rename from java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
rename to java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java
index 87d6bac2..71086a9 100644
--- a/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
+++ b/java/com/google/gerrit/server/config/EnableReverseDnsLookupProvider.java
@@ -18,16 +18,16 @@
 import com.google.inject.Provider;
 import org.eclipse.jgit.lib.Config;
 
-public class DisableReverseDnsLookupProvider implements Provider<Boolean> {
-  private final boolean disableReverseDnsLookup;
+public class EnableReverseDnsLookupProvider implements Provider<Boolean> {
+  private final Boolean enableReverseDnsLookup;
 
   @Inject
-  DisableReverseDnsLookupProvider(@GerritServerConfig Config config) {
-    disableReverseDnsLookup = config.getBoolean("gerrit", null, "disableReverseDnsLookup", false);
+  EnableReverseDnsLookupProvider(@GerritServerConfig Config config) {
+    enableReverseDnsLookup = config.getBoolean("gerrit", null, "enableReverseDnsLookup", false);
   }
 
   @Override
   public Boolean get() {
-    return disableReverseDnsLookup;
+    return enableReverseDnsLookup;
   }
 }
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6eb4ad9..f168ef9 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.AgreementSignupListener;
 import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -75,14 +76,13 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.EmailExpander;
@@ -98,6 +98,7 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
@@ -116,6 +117,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
@@ -168,7 +170,6 @@
 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;
@@ -222,7 +223,6 @@
     bind(IdGenerator.class);
     bind(RulesCache.class);
     bind(BlameCache.class).to(BlameCacheImpl.class);
-    bind(Sequences.class);
     install(authModule);
     install(AccountCacheImpl.module());
     install(BatchUpdate.module());
@@ -238,6 +238,7 @@
     install(SubmitStrategy.module());
     install(TagCache.module());
     install(OAuthTokenCache.module());
+    install(PureRevertCache.module());
 
     install(new AccessControlModule());
     install(new CmdLineParserModule());
@@ -254,8 +255,6 @@
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
 
-    bind(AccountResolver.class);
-
     factory(AddReviewerSender.Factory.class);
     factory(DeleteReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
@@ -300,8 +299,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);
@@ -314,6 +313,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);
@@ -406,9 +406,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/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index a52c076..d8c8468 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -61,8 +61,7 @@
   }
 
   public ImmutableList<Path> getAllBasePaths() {
-    return cfg.getSubsections(SECTION_NAME)
-        .stream()
+    return cfg.getSubsections(SECTION_NAME).stream()
         .map(sub -> cfg.getString(SECTION_NAME, sub, BASE_PATH_NAME))
         .filter(Objects::nonNull)
         .map(Paths::get)
@@ -90,8 +89,7 @@
    */
   @Nullable
   private String findSubSection(String project) {
-    return cfg.getSubsections(SECTION_NAME)
-        .stream()
+    return cfg.getSubsections(SECTION_NAME).stream()
         .filter(ss -> isMatch(ss, project))
         .max(comparing(String::length))
         .orElse(null);
diff --git a/java/com/google/gerrit/server/config/ScheduleConfig.java b/java/com/google/gerrit/server/config/ScheduleConfig.java
index c5f53b3..963107a2 100644
--- a/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -91,7 +91,7 @@
  *       executions are {@code Wed 10:30}, {@code Fri 10:30}. etc.
  *   <li>
  *       <pre>
- * foo.startTime = 6:00
+ * foo.startTime = 06:00
  * foo.interval = 1 day
  * </pre>
  *       Assuming that the server is started on {@code Mon 7:00} then this yields the first run on
@@ -174,7 +174,18 @@
       return true;
     }
 
-    if (interval <= 0 || initialDelay < 0) {
+    if (interval != INVALID_CONFIG && interval <= 0) {
+      logger.atSevere().log("Invalid interval value \"%d\" for \"%s\": must be > 0", interval, key);
+      interval = INVALID_CONFIG;
+    }
+
+    if (initialDelay != INVALID_CONFIG && initialDelay < 0) {
+      logger.atSevere().log(
+          "Invalid initial delay value \"%d\" for \"%s\": must be >= 0", initialDelay, key);
+      initialDelay = INVALID_CONFIG;
+    }
+
+    if (interval == INVALID_CONFIG || initialDelay == INVALID_CONFIG) {
       logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key);
       return true;
     }
@@ -216,6 +227,9 @@
       return ConfigUtil.getTimeUnit(
           rc, section, subsection, keyInterval, MISSING_CONFIG, TimeUnit.MILLISECONDS);
     } catch (IllegalArgumentException e) {
+      // We only need to log the exception message; it already includes the
+      // section.subsection.key and bad value.
+      logger.atSevere().log("%s", e.getMessage());
       return INVALID_CONFIG;
     }
   }
@@ -258,6 +272,7 @@
       }
       return delay;
     } catch (DateTimeParseException e) {
+      logger.atSevere().log("Invalid start time: %s", e.getMessage());
       return INVALID_CONFIG;
     }
   }
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/TrackingFootersProvider.java b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index ff1910d..2114b1a 100644
--- a/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -33,6 +32,8 @@
 public class TrackingFootersProvider implements Provider<TrackingFooters> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private static final int MAX_LENGTH = 10;
+
   private static String TRACKING_ID_TAG = "trackingid";
   private static String FOOTER_TAG = "footer";
   private static String SYSTEM_TAG = "system";
@@ -59,11 +60,11 @@
         configValid = false;
         logger.atSevere().log(
             "Missing %s.%s.%s in gerrit.config", TRACKING_ID_TAG, name, SYSTEM_TAG);
-      } else if (system.length() > TrackingId.TRACKING_SYSTEM_MAX_CHAR) {
+      } else if (system.length() > MAX_LENGTH) {
         configValid = false;
         logger.atSevere().log(
             "String too long \"%s\" in gerrit.config %s.%s.%s (max %d char)",
-            system, TRACKING_ID_TAG, name, SYSTEM_TAG, TrackingId.TRACKING_SYSTEM_MAX_CHAR);
+            system, TRACKING_ID_TAG, name, SYSTEM_TAG, MAX_LENGTH);
       }
 
       String match = cfg.getString(TRACKING_ID_TAG, name, REGEX_TAG);
diff --git a/java/com/google/gerrit/server/config/UrlFormatter.java b/java/com/google/gerrit/server/config/UrlFormatter.java
index 5cec1ac..066a3ca 100644
--- a/java/com/google/gerrit/server/config/UrlFormatter.java
+++ b/java/com/google/gerrit/server/config/UrlFormatter.java
@@ -40,16 +40,15 @@
   Optional<String> getWebUrl();
 
   /** Returns the URL for viewing a change. */
-  default Optional<String> getChangeViewUrl(@Nullable Project.NameKey project, Change.Id id) {
+  default Optional<String> getChangeViewUrl(Project.NameKey project, Change.Id id) {
 
     // In the PolyGerrit URL (contrary to REST URLs) there is no need to URL-escape strings, since
     // the /+/ separator unambiguously defines how to parse the path.
-    return getWebUrl()
-        .map(url -> url + "c/" + (project != null ? project.get() + "/+/" : "") + id.get());
+    return getWebUrl().map(url -> url + "c/" + project.get() + "/+/" + id.get());
   }
 
   /** Returns a URL pointing to a section of the settings page. */
-  default Optional<String> getSettingsUrl(String section) {
+  default Optional<String> getSettingsUrl(@Nullable String section) {
     return getWebUrl()
         .map(url -> url + "settings" + (Strings.isNullOrEmpty(section) ? "" : "#" + section));
   }
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 4d1811d..6295e2d 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -23,6 +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.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -42,7 +43,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -113,7 +113,7 @@
    * @throws PermissionBackendException
    */
   public void createEdit(Repository repository, ChangeNotes notes)
-      throws AuthException, IOException, InvalidChangeOperationException, OrmException,
+      throws AuthException, IOException, InvalidChangeOperationException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
@@ -141,8 +141,8 @@
    * @throws PermissionBackendException
    */
   public void rebaseEdit(Repository repository, ChangeNotes notes)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          MergeConflictException, PermissionBackendException, ResourceConflictException {
+      throws AuthException, InvalidChangeOperationException, IOException, MergeConflictException,
+          PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -206,7 +206,7 @@
    * @throws BadRequestException if the commit message is malformed
    */
   public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
-      throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
+      throws AuthException, IOException, UnchangedCommitMessageException,
           PermissionBackendException, BadRequestException, ResourceConflictException {
     assertCanEdit(notes);
     newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
@@ -249,7 +249,7 @@
    */
   public void modifyFile(
       Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent));
   }
@@ -267,7 +267,7 @@
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void deleteFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new DeleteFileModification(file));
   }
@@ -288,7 +288,7 @@
    */
   public void renameFile(
       Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath));
   }
@@ -306,14 +306,14 @@
    * @throws PermissionBackendException
    */
   public void restoreFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+      throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RestoreFileModification(file));
   }
 
   private void modifyTree(
       Repository repository, ChangeNotes notes, TreeModification treeModification)
-      throws AuthException, IOException, OrmException, InvalidChangeOperationException,
+      throws AuthException, IOException, InvalidChangeOperationException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
@@ -359,7 +359,7 @@
       PatchSet patchSet,
       List<TreeModification> treeModifications)
       throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
-          OrmException, PermissionBackendException, ResourceConflictException {
+          PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -390,17 +390,15 @@
   }
 
   private void assertCanEdit(ChangeNotes notes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
-          OrmException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
 
     Change c = notes.getChange();
-    if (!c.getStatus().isOpen()) {
+    if (!c.isNew()) {
       throw new ResourceConflictException(
-          String.format(
-              "change %s is %s", c.getChangeId(), c.getStatus().toString().toLowerCase()));
+          String.format("change %s is %s", c.getChangeId(), ChangeUtil.status(c)));
     }
 
     // Not allowed to edit if the current patch set is locked.
@@ -442,13 +440,12 @@
     return changeEditUtil.byChange(notes);
   }
 
-  private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes)
-      throws OrmException {
+  private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes) {
     Optional<PatchSet> editBasePatchSet = optionalChangeEdit.map(ChangeEdit::getBasePatchSet);
     return editBasePatchSet.isPresent() ? editBasePatchSet.get() : lookupCurrentPatchSet(notes);
   }
 
-  private PatchSet lookupCurrentPatchSet(ChangeNotes notes) throws OrmException {
+  private PatchSet lookupCurrentPatchSet(ChangeNotes notes) {
     return patchSetUtil.current(notes);
   }
 
@@ -619,7 +616,7 @@
     return user.newRefLogIdent(timestamp, tz);
   }
 
-  private void reindex(Change change) throws IOException {
+  private void reindex(Change change) {
     indexer.index(change);
   }
 }
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index d6fdf56..cb8147c 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,14 +16,11 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -32,6 +29,7 @@
 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;
@@ -41,7 +39,6 @@
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -148,9 +145,7 @@
    * @param edit change edit to publish
    * @param notify Notify handling that defines to whom email notifications should be sent after the
    *     change edit is published.
-   * @param accountsToNotify Accounts that should be notified after the change edit is published.
    * @throws IOException
-   * @throws OrmException
    * @throws UpdateException
    * @throws RestApiException
    */
@@ -158,10 +153,9 @@
       BatchUpdate.Factory updateFactory,
       ChangeNotes notes,
       CurrentUser user,
-      final ChangeEdit edit,
-      NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
-      throws IOException, OrmException, RestApiException, UpdateException {
+      ChangeEdit edit,
+      NotifyResolver.Result notify)
+      throws IOException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         ObjectInserter oi = repo.newObjectInserter();
@@ -174,11 +168,7 @@
 
       RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
       PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
-      PatchSetInserter inserter =
-          patchSetInserterFactory
-              .create(notes, psId, squashed)
-              .setNotify(notify)
-              .setAccountsToNotify(accountsToNotify);
+      PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, squashed);
 
       StringBuilder message =
           new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
@@ -199,6 +189,7 @@
 
       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(),
@@ -218,9 +209,8 @@
    *
    * @param edit change edit to delete
    * @throws IOException
-   * @throws OrmException
    */
-  public void delete(ChangeEdit edit) throws IOException, OrmException {
+  public void delete(ChangeEdit edit) throws IOException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject())) {
       deleteRef(repo, edit);
@@ -234,7 +224,7 @@
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
       return psUtil.get(notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
-    } catch (OrmException | NumberFormatException e) {
+    } catch (StorageException | NumberFormatException e) {
       throw new IOException(e);
     }
   }
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index f17a0f0..d5f548f 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -77,8 +76,7 @@
   }
 
   @Override
-  public void postEvent(Change change, ChangeEvent event)
-      throws OrmException, PermissionBackendException {
+  public void postEvent(Change change, ChangeEvent event) throws PermissionBackendException {
     fireEvent(change, event);
   }
 
@@ -94,7 +92,7 @@
   }
 
   @Override
-  public void postEvent(Event event) throws OrmException, PermissionBackendException {
+  public void postEvent(Event event) throws PermissionBackendException {
     fireEvent(event);
   }
 
@@ -102,10 +100,9 @@
     unrestrictedListeners.runEach(l -> l.onEvent(event));
   }
 
-  protected void fireEvent(Change change, ChangeEvent event)
-      throws OrmException, PermissionBackendException {
+  protected void fireEvent(Change change, ChangeEvent event) throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(change, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -115,7 +112,7 @@
 
   protected void fireEvent(Project.NameKey project, ProjectEvent event) {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(project, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -126,7 +123,7 @@
   protected void fireEvent(Branch.NameKey branchName, RefEvent event)
       throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(branchName, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -134,9 +131,9 @@
     fireEventForUnrestrictedListeners(event);
   }
 
-  protected void fireEvent(Event event) throws OrmException, PermissionBackendException {
+  protected void fireEvent(Event event) throws PermissionBackendException {
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
-      CurrentUser user = c.call(l -> l.getUser());
+      CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(event, user)) {
         c.run(l -> l.onEvent(event));
       }
@@ -158,8 +155,7 @@
     }
   }
 
-  protected boolean isVisibleTo(Change change, CurrentUser user)
-      throws OrmException, PermissionBackendException {
+  protected boolean isVisibleTo(Change change, CurrentUser user) throws PermissionBackendException {
     if (change == null) {
       return false;
     }
@@ -193,8 +189,7 @@
     }
   }
 
-  protected boolean isVisibleTo(Event event, CurrentUser user)
-      throws OrmException, PermissionBackendException {
+  protected boolean isVisibleTo(Event event, CurrentUser user) throws PermissionBackendException {
     if (event instanceof RefEvent) {
       RefEvent refEvent = (RefEvent) event;
       String ref = refEvent.getRefName();
diff --git a/java/com/google/gerrit/server/events/EventDispatcher.java b/java/com/google/gerrit/server/events/EventDispatcher.java
index cbf547e..e6735f2 100644
--- a/java/com/google/gerrit/server/events/EventDispatcher.java
+++ b/java/com/google/gerrit/server/events/EventDispatcher.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 
 /** Interface for posting (dispatching) Events */
 public interface EventDispatcher {
@@ -27,10 +26,9 @@
    *
    * @param change The change that the event is related to
    * @param event The event to post
-   * @throws OrmException on failure to post the event due to DB error
    * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Change change, ChangeEvent event) throws OrmException, PermissionBackendException;
+  void postEvent(Change change, ChangeEvent event) throws PermissionBackendException;
 
   /**
    * Post a stream event that is related to a branch
@@ -56,8 +54,7 @@
    * specific postEvent methods for those use cases.
    *
    * @param event The event to post.
-   * @throws OrmException on failure to post the event due to DB error
    * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Event event) throws OrmException, PermissionBackendException;
+  void postEvent(Event event) throws PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index fc803c8..efd9bb9 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -62,7 +64,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -83,7 +84,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AccountCache accountCache;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final Emails emails;
   private final PatchListCache patchListCache;
   private final Provider<PersonIdent> myIdent;
@@ -97,7 +98,7 @@
   EventFactory(
       AccountCache accountCache,
       Emails emails,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       PatchListCache patchListCache,
       @GerritPersonIdent Provider<PersonIdent> myIdent,
       ChangeData.Factory changeDataFactory,
@@ -154,7 +155,7 @@
    * @param notes
    * @return object suitable for serialization to JSON
    */
-  public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) throws OrmException {
+  public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
     ChangeAttribute a = asChangeAttribute(change);
     Set<String> hashtags = notes.load().getHashtags();
     if (!hashtags.isEmpty()) {
@@ -190,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();
   }
 
   /**
@@ -199,7 +200,7 @@
    * @param a
    * @param notes
    */
-  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes) throws OrmException {
+  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes) {
     Collection<Account.Id> reviewers = approvalsUtil.getReviewers(notes).all();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
@@ -270,7 +271,7 @@
     try {
       addDependsOn(rw, ca, change, currentPs);
       addNeededBy(rw, ca, change, currentPs);
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       // Squash DB exceptions and leave dependency lists partially filled.
     }
     // Remove empty lists so a confusing label won't be displayed in the output.
@@ -283,7 +284,7 @@
   }
 
   private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
-      throws OrmException, IOException {
+      throws IOException {
     RevCommit commit = rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
     final List<String> parentNames = new ArrayList<>(commit.getParentCount());
     for (RevCommit p : commit.getParents()) {
@@ -316,7 +317,7 @@
   }
 
   private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
-      throws OrmException, IOException {
+      throws IOException {
     if (currentPs.getGroups().isEmpty()) {
       return;
     }
@@ -493,7 +494,7 @@
         }
       }
       p.kind = changeKindCache.getChangeKind(change, patchSet);
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
     } catch (PatchListObjectTooLargeException e) {
       logger.atWarning().log("Cannot get size information for %s: %s", pId, e.getMessage());
@@ -505,7 +506,7 @@
 
   // TODO: The same method exists in PatchSetInfoFactory, find a common place
   // for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
     UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
@@ -634,7 +635,7 @@
   /** Get a link to the change; null if the server doesn't know its own address. */
   private String getChangeUrl(Change change) {
     if (change != null) {
-      return urlFormatter.getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
+      return urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
     }
     return null;
   }
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index faddfe6..3add4ca 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -55,7 +56,6 @@
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -63,7 +63,6 @@
 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;
@@ -136,15 +135,15 @@
     this.changeNotesFactory = changeNotesFactory;
   }
 
-  private ChangeNotes getNotes(ChangeInfo info) throws OrmException {
+  private ChangeNotes getNotes(ChangeInfo info) {
     try {
       return changeNotesFactory.createChecked(new Change.Id(info._number));
     } catch (NoSuchChangeException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info) throws OrmException {
+  private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info) {
     return psUtil.get(notes, PatchSet.Id.fromRef(info.ref));
   }
 
@@ -153,7 +152,7 @@
         () -> {
           try {
             return eventFactory.asChangeAttribute(change, notes);
-          } catch (OrmException e) {
+          } catch (StorageException e) {
             throw new RuntimeException(e);
           }
         });
@@ -182,7 +181,7 @@
 
   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);
     }
@@ -190,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();
 
@@ -220,7 +219,7 @@
           if (approvals.size() > 0) {
             ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()];
             int i = 0;
-            for (Entry<String, Short> approval : approvals.entrySet()) {
+            for (Map.Entry<String, Short> approval : approvals.entrySet()) {
               r[i++] =
                   getApprovalAttribute(labelTypes, approval, convertApprovalsMap(oldApprovals));
             }
@@ -249,7 +248,7 @@
       event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -266,7 +265,7 @@
       event.oldTopic = ev.getOldTopic();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -284,7 +283,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -304,7 +303,7 @@
           approvalsAttributeSupplier(change, ev.getNewApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -322,7 +321,7 @@
         event.reviewer = accountAttributeSupplier(reviewer);
         dispatcher.run(d -> d.postEvent(event));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -350,7 +349,7 @@
       event.removed = hashtagArray(ev.getRemovedHashtags());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -387,7 +386,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -405,7 +404,7 @@
       event.reason = ev.getReason();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -423,7 +422,7 @@
       event.newRev = ev.getNewRevisionId();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -441,7 +440,7 @@
       event.reason = ev.getReason();
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -459,7 +458,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -477,7 +476,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -497,7 +496,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
@@ -513,7 +512,7 @@
       event.deleter = accountAttributeSupplier(ev.getWho());
 
       dispatcher.run(d -> d.postEvent(change, event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index 513a5de..fdce1da 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -53,7 +53,7 @@
               util.accountInfo(oldAssignee),
               when);
       listeners.runEach(l -> l.onAssigneeChanged(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index 3d6700e..a8c08b9 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -72,7 +72,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
index d9eb9f9..9e3e979 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -47,7 +47,7 @@
     try {
       Event event = new Event(util.changeInfo(change), util.accountInfo(deleter), when);
       listeners.runEach(l -> l.onChangeDeleted(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 7b814ae..756f383 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -66,7 +66,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 81b04cd..e8bed56 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -66,7 +66,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index ac7aac0..ccb17d5 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeRevertedListener;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -45,7 +45,7 @@
     try {
       Event event = new Event(util.changeInfo(change), util.changeInfo(revertChange), when);
       listeners.runEach(l -> l.onChangeReverted(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index e224540..ea9ae31 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -76,7 +76,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 30450dd..d00eb31 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -74,19 +73,17 @@
     this.revisionJsonFactory = revisionJsonFactory;
   }
 
-  public ChangeInfo changeInfo(Change change) throws OrmException {
+  public ChangeInfo changeInfo(Change change) {
     return changeJsonFactory.create(CHANGE_OPTIONS).format(change);
   }
 
   public RevisionInfo revisionInfo(Project project, PatchSet ps)
-      throws OrmException, PatchListNotAvailableException, GpgException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     return revisionInfo(project.getNameKey(), ps);
   }
 
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
-      throws OrmException, PatchListNotAvailableException, GpgException, IOException,
-          PermissionBackendException {
+      throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(project, ps.getId().getParentKey());
     return revisionJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
   }
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index ca0edab..65f5b8b 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -23,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -58,7 +58,7 @@
           new Event(
               util.changeInfo(change), util.accountInfo(editor), hashtags, added, removed, when);
       listeners.runEach(l -> l.onHashtagsEdited(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index 358667f..49a6091 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -58,7 +58,7 @@
               util.accountInfo(account),
               when);
       listeners.runEach(l -> l.onPrivateStateChanged(event));
-    } catch (OrmException
+    } catch (StorageException
         | PatchListNotAvailableException
         | GpgException
         | IOException
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 8e5259c..f9f67f6 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -73,7 +73,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 89c8f18..b92f3e6 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -80,7 +80,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index e043e9f..6fddcfe 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -24,11 +25,11 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -46,7 +47,7 @@
             PatchSet patchSet,
             AccountState uploader,
             Timestamp when,
-            NotifyHandling notify) {}
+            NotifyResolver.Result notify) {}
       };
 
   private final PluginSetContext<RevisionCreatedListener> listeners;
@@ -68,7 +69,7 @@
       PatchSet patchSet,
       AccountState uploader,
       Timestamp when,
-      NotifyHandling notify) {
+      NotifyResolver.Result notify) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -79,14 +80,14 @@
               util.revisionInfo(change.getProject(), patchSet),
               util.accountInfo(uploader),
               when,
-              notify);
+              notify.handling());
       listeners.runEach(l -> l.onRevisionCreated(event));
     } catch (PatchListObjectTooLargeException e) {
       logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 8568c0f..9e1ae44 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
@@ -48,7 +48,7 @@
       Event event =
           new Event(util.changeInfo(change), util.accountInfo(account), oldTopicName, when);
       listeners.runEach(l -> l.onTopicEdited(event));
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index b750851..bd6873a 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -80,7 +80,7 @@
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
-        | OrmException
+        | StorageException
         | PermissionBackendException e) {
       logger.atSevere().withCause(e).log("Couldn't fire event");
     }
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index 6273ad6..785d6fe 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -59,7 +59,7 @@
               util.accountInfo(account),
               when);
       listeners.runEach(l -> l.onWorkInProgressStateChanged(event));
-    } catch (OrmException
+    } catch (StorageException
         | PatchListNotAvailableException
         | GpgException
         | IOException
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 3cb771e..d8aeece 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -75,7 +75,7 @@
   private final GitRepositoryManager repoManager;
   private final TimeZone tz;
   private final PermissionBackend permissionBackend;
-  private NotesBranchUtil.Factory notesBranchUtilFactory;
+  private final NotesBranchUtil.Factory notesBranchUtilFactory;
 
   @Inject
   BanCommit(
diff --git a/java/com/google/gerrit/server/git/ChangeRefCache.java b/java/com/google/gerrit/server/git/ChangeRefCache.java
deleted file mode 100644
index dc1b946..0000000
--- a/java/com/google/gerrit/server/git/ChangeRefCache.java
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-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.client.RefNames;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.config.GerritOptions;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.change.ChangeField;
-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.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.util.ManualRequestContext;
-import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * Cache for the minimal information per change that we need to compute visibility. Used for ref
- * filtering.
- *
- * <p>This class is thread safe.
- */
-@Singleton
-public class ChangeRefCache {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  static final String ID_CACHE = "change_refs";
-
-  public static class Module extends CacheModule {
-    @Override
-    protected void configure() {
-      cache(ID_CACHE, Key.class, new TypeLiteral<CachedChange>() {})
-          .maximumWeight(10000)
-          .loader(Loader.class);
-
-      bind(ChangeRefCache.class);
-    }
-  }
-
-  @AutoValue
-  abstract static class Key {
-    abstract Project.NameKey project();
-
-    abstract Change.Id changeId();
-
-    abstract ObjectId metaId();
-  }
-
-  @AutoValue
-  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.
-    abstract Change change();
-
-    @Nullable
-    abstract ReviewerSet reviewers();
-  }
-
-  private final LoadingCache<Key, CachedChange> cache;
-  private final ChangeData.Factory changeDataFactory;
-  private final OneOffRequestContext requestContext;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final GerritOptions gerritOptions;
-  private final Config gerritConfig;
-  private final Set<Project.NameKey> bootstrappedProjects;
-
-  @Inject
-  ChangeRefCache(
-      @Named(ID_CACHE) LoadingCache<Key, CachedChange> cache,
-      ChangeData.Factory changeDataFactory,
-      OneOffRequestContext requestContext,
-      Provider<InternalChangeQuery> queryProvider,
-      GerritOptions gerritOptions,
-      @GerritServerConfig Config gerritConfig) {
-    this.cache = cache;
-    this.changeDataFactory = changeDataFactory;
-    this.requestContext = requestContext;
-    this.queryProvider = queryProvider;
-    this.gerritOptions = gerritOptions;
-    this.gerritConfig = gerritConfig;
-    // Uses a CopyOnWriteArraySet internally to keep track of projects that are already
-    // bootstrapped. This is efficient because we read from the set on every call to this method to
-    // check if bootstrapping is required. Writes occur only if we bootstrapped, so once per
-    // project.
-    this.bootstrappedProjects = new CopyOnWriteArraySet<>();
-  }
-
-  /**
-   * Read changes from the cache.
-   *
-   * <p>Returned changes only include the {@code Change} object (with id, branch) and the reviewers.
-   * There is no guarantee that additional fields are populated, although they can be.
-   *
-   * @param project project to read.
-   * @param changeId change ID to read
-   * @param metaId object ID of the meta branch to read. This is only used to ensure consistency. It
-   *     does not allow for reading non-current meta versions.
-   * @return change data
-   * @throws IllegalArgumentException in case no change is found
-   */
-  public ChangeData getChangeData(Project.NameKey project, Change.Id changeId, ObjectId metaId) {
-    Key key = new AutoValue_ChangeRefCache_Key(project, changeId, metaId);
-    CachedChange cached = cache.getUnchecked(key);
-    if (cached == null) {
-      throw new IllegalArgumentException("no change found for key " + key);
-    }
-    ChangeData cd = changeDataFactory.create(cached.change());
-    cd.setReviewers(cached.reviewers());
-    return cd;
-  }
-
-  /**
-   * This method bootstraps the cache by querying the change index if it hasn't been bootstrapped
-   * before, in which case it is a cheap no-op.
-   *
-   * @param project the project to bootstrap
-   */
-  public void bootstrapIfNecessary(Project.NameKey project) {
-    if (!gerritOptions.enableMasterFeatures()) {
-      // Bootstrapping using the ChangeIndex is only supported on master in a master-slave replica.
-      return;
-    }
-    if (gerritConfig.getInt("cache", ID_CACHE, "memoryLimit", -1) == 0) {
-      // The cache is disabled, don't bother bootstrapping.
-      return;
-    }
-    if (bootstrappedProjects.contains(project)) {
-      // We have bootstrapped for this project before. If the cache is too small, we might have
-      // evicted all entries by now. Don't bother about this though as we don't want to add the
-      // complexity of checking for existing projects, since that might not be authoritative as well
-      // since we could have already evicted the majority of the entries.
-      return;
-    }
-
-    try (TraceTimer ignored =
-            TraceContext.newTimer("bootstrapping ChangeRef cache for project " + project);
-        ManualRequestContext ignored2 = requestContext.open()) {
-      List<ChangeData> cds =
-          queryProvider
-              .get()
-              .setRequestedFields(ChangeField.CHANGE, ChangeField.REVIEWER, ChangeField.REF_STATE)
-              .byProject(project);
-      for (ChangeData cd : cds) {
-        Set<RefState> refStates = RefState.parseStates(cd.getRefStates()).get(project);
-        Optional<RefState> refState =
-            refStates
-                .stream()
-                .filter(r -> r.ref().equals(RefNames.changeMetaRef(cd.getId())))
-                .findAny();
-        if (!refState.isPresent()) {
-          continue;
-        }
-        cache.put(
-            new AutoValue_ChangeRefCache_Key(project, cd.change().getId(), refState.get().id()),
-            new AutoValue_ChangeRefCache_CachedChange(cd.change(), cd.getReviewers()));
-      }
-      // Mark the project as bootstrapped. We could have bootstrapped it multiple times for
-      // simultaneous requests. We accept this in favor of less thread synchronization and
-      // complexity.
-      bootstrappedProjects.add(project);
-    } catch (OrmException e) {
-      logger.atWarning().withCause(e).log(
-          "unable to bootstrap ChangeRef cache for project " + project);
-    }
-  }
-
-  static class Loader extends CacheLoader<Key, CachedChange> {
-    private final ChangeNotes.Factory notesFactory;
-
-    @Inject
-    Loader(ChangeNotes.Factory notesFactory) {
-      this.notesFactory = notesFactory;
-    }
-
-    @Override
-    public CachedChange load(Key key) throws Exception {
-      ChangeNotes notes = notesFactory.create(key.project(), key.changeId());
-      if (notes.getMetaId().equals(key.metaId())) {
-        return new AutoValue_ChangeRefCache_CachedChange(notes.getChange(), notes.getReviewers());
-      }
-      throw new NoSuchElementException("unable to load change");
-    }
-  }
-
-  @VisibleForTesting
-  public void resetBootstrappedProjects() {
-    bootstrappedProjects.clear();
-  }
-}
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 1241585..fc9abb4 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.inject.Inject;
@@ -26,17 +27,18 @@
   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 UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
-  DefaultChangeReportFormatter(UrlFormatter urlFormatter) {
+  DefaultChangeReportFormatter(DynamicItem<UrlFormatter> urlFormatter) {
     this.urlFormatter = urlFormatter;
   }
 
   @Override
   public String newChange(ChangeReportFormatter.Input input) {
-    return formatChangeUrl(input);
+    return formatChangeUrl(input) + NEW_CHANGE_INDICATOR;
   }
 
   @Override
@@ -49,7 +51,10 @@
     Change c = input.change();
     return String.format(
         "change %s closed",
-        urlFormatter.getChangeViewUrl(c.getProject(), c.getId()).orElse(c.getId().toString()));
+        urlFormatter
+            .get()
+            .getChangeViewUrl(c.getProject(), c.getId())
+            .orElse(c.getId().toString()));
   }
 
   protected String cropSubject(String subject) {
@@ -69,7 +74,7 @@
 
   protected String formatChangeUrl(Input input) {
     Change c = input.change();
-    Optional<String> changeUrl = urlFormatter.getChangeViewUrl(c.getProject(), c.getId());
+    Optional<String> changeUrl = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
     checkState(changeUrl.isPresent());
 
     StringBuilder m =
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index 3f037d2..07c995f 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
@@ -89,7 +88,7 @@
   }
 
   private interface Lookup {
-    List<String> lookup(PatchSet.Id psId) throws OrmException;
+    List<String> lookup(PatchSet.Id psId);
   }
 
   private final ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha;
@@ -198,7 +197,7 @@
     }
   }
 
-  public SortedSetMultimap<ObjectId, String> getGroups() throws OrmException {
+  public SortedSetMultimap<ObjectId, String> getGroups() {
     done = true;
     SortedSetMultimap<ObjectId, String> result =
         MultimapBuilder.hashKeys(groups.keySet().size()).treeSetValues().build();
@@ -224,8 +223,7 @@
     return id != null && patchSetsBySha.containsKey(id);
   }
 
-  private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates)
-      throws OrmException {
+  private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates) {
     Set<String> actual = Sets.newTreeSet();
     Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size());
     Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size());
@@ -260,7 +258,7 @@
     }
   }
 
-  private Iterable<String> resolveGroup(ObjectId forCommit, String group) throws OrmException {
+  private Iterable<String> resolveGroup(ObjectId forCommit, String group) {
     ObjectId id = parseGroup(forCommit, group);
     if (id != null) {
       PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
diff --git a/java/com/google/gerrit/server/git/HookUtil.java b/java/com/google/gerrit/server/git/HookUtil.java
index 27c6e1e..6e8f27c 100644
--- a/java/com/google/gerrit/server/git/HookUtil.java
+++ b/java/com/google/gerrit/server/git/HookUtil.java
@@ -40,10 +40,7 @@
     }
     try {
       refs =
-          rp.getRepository()
-              .getRefDatabase()
-              .getRefs()
-              .stream()
+          rp.getRepository().getRefDatabase().getRefs().stream()
               .collect(toMap(Ref::getName, r -> r));
     } catch (ServiceMayNotContinueException e) {
       throw e;
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index ee4d71e..5e2ad47 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -30,7 +30,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -41,7 +44,6 @@
 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.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -55,7 +57,6 @@
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.submit.MergeSorter;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -125,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;
     }
   }
@@ -160,7 +172,7 @@
   }
 
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final ApprovalsUtil approvalsUtil;
   private final ProjectState project;
   private final boolean useContentMerge;
@@ -171,7 +183,7 @@
   MergeUtil(
       @GerritServerConfig Config serverConfig,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
       PluggableCommitMessageGenerator commitMessageGenerator,
       @Assisted ProjectState project) {
@@ -189,7 +201,7 @@
   MergeUtil(
       @GerritServerConfig Config serverConfig,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
       @Assisted ProjectState project,
       PluggableCommitMessageGenerator commitMessageGenerator,
@@ -225,7 +237,7 @@
     List<CodeReviewCommit> result = new ArrayList<>();
     try {
       result.addAll(mergeSorter.sort(toSort));
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
     result.sort(CodeReviewCommit.ORDER);
@@ -282,9 +294,7 @@
           ((ResolveMerger) m).getMergeResults();
 
       filesWithGitConflicts =
-          mergeResults
-              .entrySet()
-              .stream()
+          mergeResults.entrySet().stream()
               .filter(e -> e.getValue().containsConflicts())
               .map(Map.Entry::getKey)
               .collect(toImmutableSet());
@@ -306,6 +316,7 @@
     return commit;
   }
 
+  @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
   public static ObjectId mergeWithConflicts(
       RevWalk rw,
       ObjectInserter ins,
@@ -339,13 +350,20 @@
     Map<String, ObjectId> resolved = new HashMap<>();
     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)) {
+      TemporaryBuffer buf = null;
+      try {
+        // TODO(dborowitz): Respect inCoreLimit here.
+        buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
         fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
-        buf.close();
+        buf.close(); // Flush file and close for writes, but leave available for reading.
 
         try (InputStream in = buf.openInputStream()) {
           resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
         }
+      } finally {
+        if (buf != null) {
+          buf.destroy();
+        }
       }
     }
 
@@ -483,7 +501,7 @@
       msgbuf.append('\n');
     }
 
-    Optional<String> url = urlFormatter.getChangeViewUrl(null, c.getId());
+    Optional<String> url = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
     if (url.isPresent()) {
       if (!contains(footers, FooterConstants.REVIEWED_ON, url.get())) {
         msgbuf
@@ -575,7 +593,7 @@
    * @return new message
    */
   public String createCommitMessageOnSubmit(
-      RevCommit n, RevCommit mergeTip, ChangeNotes notes, Id id) {
+      RevCommit n, RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
     return commitMessageGenerator.generate(
         n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
   }
@@ -591,7 +609,7 @@
   private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
     try {
       return approvalsUtil.byPatchSet(notes, psId, null, null);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
       return Collections.emptyList();
     }
@@ -704,7 +722,7 @@
       throws IntegrationException {
     try {
       return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
   }
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index e63c646..43dc061 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -105,7 +104,7 @@
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) throws OrmException, IOException {
+  public boolean updateChange(ChangeContext ctx) throws IOException {
     change = ctx.getChange();
     correctBranch = refName.equals(change.getDest().get());
     if (!correctBranch) {
@@ -125,8 +124,7 @@
     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);
@@ -194,7 +192,7 @@
         change, patchSet, ctx.getAccount(), patchSet.getRevision().get(), ctx.getWhen());
   }
 
-  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException, OrmException {
+  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {
     RevWalk rw = ctx.getRevWalk();
     RevCommit commit =
         rw.parseCommit(ObjectId.fromString(requireNonNull(patchSet).getRevision().get()));
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
new file mode 100644
index 0000000..1daa1d5
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.proto.Cache;
+import com.google.gerrit.server.cache.proto.Cache.PureRevertKeyProto;
+import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Computes and caches if a change is a pure revert of another change. */
+@Singleton
+public class PureRevertCache {
+  private static final String ID_CACHE = "pure_revert";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(ID_CACHE, Cache.PureRevertKeyProto.class, Boolean.class)
+            .maximumWeight(100)
+            .loader(Loader.class)
+            .version(1)
+            .keySerializer(new ProtobufSerializer<>(Cache.PureRevertKeyProto.parser()))
+            .valueSerializer(BooleanCacheSerializer.INSTANCE);
+      }
+    };
+  }
+
+  private final LoadingCache<PureRevertKeyProto, Boolean> cache;
+  private final ChangeNotes.Factory notesFactory;
+
+  @Inject
+  PureRevertCache(
+      @Named(ID_CACHE) LoadingCache<PureRevertKeyProto, Boolean> cache,
+      ChangeNotes.Factory notesFactory) {
+    this.cache = cache;
+    this.notesFactory = notesFactory;
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of the change that is
+   * referenced in {@link Change#getRevertOf()}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ChangeNotes}
+   */
+  public boolean isPureRevert(ChangeNotes claimedRevert) throws IOException, BadRequestException {
+    if (claimedRevert.getChange().getRevertOf() == null) {
+      throw new BadRequestException("revertOf not set");
+    }
+    ChangeNotes claimedOriginal =
+        notesFactory.createChecked(
+            claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
+    return isPureRevert(
+        claimedRevert.getProjectName(),
+        ObjectId.fromString(claimedRevert.getCurrentPatchSet().getRevision().get()),
+        ObjectId.fromString(claimedOriginal.getCurrentPatchSet().getRevision().get()));
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   * claimedOriginal}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   *     claimedOriginal}.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ObjectId}s
+   */
+  public boolean isPureRevert(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal)
+      throws IOException, BadRequestException {
+    try {
+      return cache.get(key(project, claimedRevert, claimedOriginal));
+    } catch (ExecutionException e) {
+      Throwables.throwIfInstanceOf(e.getCause(), BadRequestException.class);
+      throw new IOException(e);
+    }
+  }
+
+  @VisibleForTesting
+  static PureRevertKeyProto key(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal) {
+    ByteString original = ObjectIdConverter.create().toByteString(claimedOriginal);
+    ByteString revert = ObjectIdConverter.create().toByteString(claimedRevert);
+    return PureRevertKeyProto.newBuilder()
+        .setProject(project.get())
+        .setClaimedOriginal(original)
+        .setClaimedRevert(revert)
+        .build();
+  }
+
+  static class Loader extends CacheLoader<PureRevertKeyProto, Boolean> {
+    private final GitRepositoryManager repoManager;
+    private final MergeUtil.Factory mergeUtilFactory;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Loader(
+        GitRepositoryManager repoManager,
+        MergeUtil.Factory mergeUtilFactory,
+        ProjectCache projectCache) {
+      this.repoManager = repoManager;
+      this.mergeUtilFactory = mergeUtilFactory;
+      this.projectCache = projectCache;
+    }
+
+    @Override
+    public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
+      try (TraceContext.TraceTimer ignored =
+          TraceContext.newTimer("Loading pure revert for %s", key)) {
+        ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
+        ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
+        Project.NameKey project = new Project.NameKey(key.getProject());
+
+        try (Repository repo = repoManager.openRepository(project);
+            ObjectInserter oi = repo.newObjectInserter();
+            RevWalk rw = new RevWalk(repo)) {
+          RevCommit claimedOriginalCommit;
+          try {
+            claimedOriginalCommit = rw.parseCommit(original);
+          } catch (InvalidObjectIdException | MissingObjectException e) {
+            throw new BadRequestException("invalid object ID");
+          }
+          if (claimedOriginalCommit.getParentCount() == 0) {
+            throw new BadRequestException("can't check against initial commit");
+          }
+          RevCommit claimedRevertCommit = rw.parseCommit(revert);
+          if (claimedRevertCommit.getParentCount() == 0) {
+            return false;
+          }
+          // Rebase claimed revert onto claimed original
+          ThreeWayMerger merger =
+              mergeUtilFactory
+                  .create(projectCache.checkedGet(project))
+                  .newThreeWayMerger(oi, repo.getConfig());
+          merger.setBase(claimedRevertCommit.getParent(0));
+          boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
+          if (!success || merger.getResultTreeId() == null) {
+            // Merge conflict during rebase
+            return false;
+          }
+
+          // Any differences between claimed original's parent and the rebase result indicate that
+          // the
+          // claimedRevert is not a pure revert but made content changes
+          try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+            df.setReader(oi.newReader(), repo.getConfig());
+            List<DiffEntry> entries =
+                df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+            return entries.isEmpty();
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
new file mode 100644
index 0000000..d7f8982
--- /dev/null
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -0,0 +1,171 @@
+// 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.git;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.cache.CacheLoader;
+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.server.ReviewerSet;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.util.Providers;
+import java.util.ArrayList;
+import java.util.Collections;
+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();
+
+  static final String ID_CACHE = "changes";
+
+  public static class Module extends CacheModule {
+    private final boolean slave;
+
+    public Module() {
+      this(false);
+    }
+
+    public Module(boolean slave) {
+      this.slave = slave;
+    }
+
+    @Override
+    protected void configure() {
+      if (slave) {
+        bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
+      } else {
+        cache(ID_CACHE, Project.NameKey.class, new TypeLiteral<List<CachedChange>>() {})
+            .maximumWeight(0)
+            .loader(Loader.class);
+
+        bind(SearchingChangeCacheImpl.class);
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+            .to(SearchingChangeCacheImpl.class);
+      }
+    }
+  }
+
+  @AutoValue
+  @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.
+    abstract Change change();
+
+    @Nullable
+    abstract ReviewerSet reviewers();
+  }
+
+  private final LoadingCache<Project.NameKey, List<CachedChange>> cache;
+  private final ChangeData.Factory changeDataFactory;
+
+  @Inject
+  SearchingChangeCacheImpl(
+      @Named(ID_CACHE) LoadingCache<Project.NameKey, List<CachedChange>> cache,
+      ChangeData.Factory changeDataFactory) {
+    this.cache = cache;
+    this.changeDataFactory = changeDataFactory;
+  }
+
+  /**
+   * Read changes for the project from the secondary index.
+   *
+   * <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 project project to read.
+   * @return list of known changes; empty if no changes.
+   */
+  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(cc.change());
+        cd.setReviewers(cc.reviewers());
+        cds.add(cd);
+      }
+      return Collections.unmodifiableList(cds);
+    } catch (ExecutionException e) {
+      logger.atWarning().withCause(e).log("Cannot fetch changes for %s", project);
+      return Collections.emptyList();
+    }
+  }
+
+  @Override
+  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
+    if (event.getRefName().startsWith(RefNames.REFS_CHANGES)) {
+      cache.invalidate(new Project.NameKey(event.getProjectName()));
+    }
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, List<CachedChange>> {
+    private final OneOffRequestContext requestContext;
+    private final Provider<InternalChangeQuery> queryProvider;
+
+    @Inject
+    Loader(OneOffRequestContext requestContext, Provider<InternalChangeQuery> queryProvider) {
+      this.requestContext = requestContext;
+      this.queryProvider = queryProvider;
+    }
+
+    @Override
+    public List<CachedChange> load(Project.NameKey key) throws Exception {
+      try (TraceTimer timer = TraceContext.newTimer("Loading changes of project %s", key);
+          ManualRequestContext ctx = requestContext.open()) {
+        List<ChangeData> cds =
+            queryProvider
+                .get()
+                .setRequestedFields(ChangeField.CHANGE, ChangeField.REVIEWER)
+                .byProject(key);
+        List<CachedChange> result = new ArrayList<>(cds.size());
+        for (ChangeData cd : cds) {
+          result.add(
+              new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.getReviewers()));
+        }
+        return Collections.unmodifiableList(result);
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 724846a..d455b82 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -392,8 +392,7 @@
     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));
     }
 
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index b33aa3c..f109570 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -17,10 +17,11 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 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.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -64,8 +65,6 @@
  * read from the repository, or format an update that can later be written back to the repository.
  */
 public abstract class VersionedMetaData {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   /**
    * Path information that does not hold references to any repository data structures, allowing the
    * application to retain this object for long periods of time.
@@ -497,10 +496,11 @@
       return new byte[] {};
     }
 
-    logger.atFine().log(
-        "Read file '%s' from ref '%s' of project '%s' from revision '%s'",
-        fileName, getRefName(), projectName, revision.name());
-    try (TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
+    try (TraceTimer timer =
+            TraceContext.newTimer(
+                "Read file '%s' from ref '%s' of project '%s' from revision '%s'",
+                fileName, getRefName(), projectName, revision.name());
+        TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
       if (tw != null) {
         ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
         return obj.getCachedBytes(Integer.MAX_VALUE);
@@ -572,22 +572,24 @@
   }
 
   protected void saveFile(String fileName, byte[] raw) throws IOException {
-    logger.atFine().log(
-        "Save file '%s' in ref '%s' of project '%s'", fileName, getRefName(), projectName);
-    DirCacheEditor editor = newTree.editor();
-    if (raw != null && 0 < raw.length) {
-      final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
-      editor.add(
-          new PathEdit(fileName) {
-            @Override
-            public void apply(DirCacheEntry ent) {
-              ent.setFileMode(FileMode.REGULAR_FILE);
-              ent.setObjectId(blobId);
-            }
-          });
-    } else {
-      editor.add(new DeletePath(fileName));
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Save file '%s' in ref '%s' of project '%s'", fileName, getRefName(), projectName)) {
+      DirCacheEditor editor = newTree.editor();
+      if (raw != null && 0 < raw.length) {
+        final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
+        editor.add(
+            new PathEdit(fileName) {
+              @Override
+              public void apply(DirCacheEntry ent) {
+                ent.setFileMode(FileMode.REGULAR_FILE);
+                ent.setObjectId(blobId);
+              }
+            });
+      } else {
+        editor.add(new DeletePath(fileName));
+      }
+      editor.finish();
     }
-    editor.finish();
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index abbba86..66e66ca 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;
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.TransferConfig;
-import com.google.gerrit.server.git.receive.ResultChangeIds.Key;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -116,17 +115,25 @@
 
   private class Worker implements ProjectRunnable {
     final MultiProgressMonitor progress;
+    final String name;
 
     private final Collection<ReceiveCommand> commands;
 
-    private Worker(Collection<ReceiveCommand> commands) {
+    private Worker(Collection<ReceiveCommand> commands, String name) {
       this.commands = commands;
+      this.name = name;
       progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
     }
 
     @Override
     public void run() {
-      receiveCommits.processCommands(commands, progress);
+      String oldName = Thread.currentThread().getName();
+      Thread.currentThread().setName(oldName + "-for-" + name);
+      try {
+        receiveCommits.processCommands(commands, progress);
+      } finally {
+        Thread.currentThread().setName(oldName);
+      }
     }
 
     @Override
@@ -176,29 +183,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(
@@ -319,7 +342,7 @@
     }
 
     long startNanos = System.nanoTime();
-    Worker w = new Worker(commands);
+    Worker w = new Worker(commands, Thread.currentThread().getName());
     try {
       w.progress.waitFor(
           executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
@@ -343,23 +366,29 @@
     long deltaNanos = System.nanoTime() - startNanos;
     int totalChanges = 0;
 
+    PushType pushType;
     if (resultChangeIds.isMagicPush()) {
-      List<Change.Id> created = resultChangeIds.get(Key.CREATED);
-      metrics.changes.record(Key.CREATED, created.size());
-      List<Change.Id> replaced = resultChangeIds.get(Key.REPLACED);
-      metrics.changes.record(Key.REPLACED, replaced.size());
-      totalChanges += replaced.size() + created.size();
+      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(Key.AUTOCLOSED);
-      metrics.changes.record(Key.AUTOCLOSED, autoclosed.size());
+      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.isMagicPush() ? "CREATE_REPLACE" : ResultChangeIds.Key.AUTOCLOSED.name(),
-          deltaNanos / totalChanges,
-          NANOSECONDS);
+      metrics.latencyPerChange.record(pushType, deltaNanos / totalChanges, NANOSECONDS);
     }
+
+    metrics.latencyPerPush.record(pushType, deltaNanos, NANOSECONDS);
   }
 
   /** Returns the Change.Ids that were processed in onPreReceive */
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index f762611..da21fbb 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -5,6 +5,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
@@ -14,7 +15,6 @@
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index c6c4819..9bad21d 100644
--- a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
+++ b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
@@ -79,10 +79,7 @@
     if (r == null) {
       try {
         r =
-            rp.getRepository()
-                .getRefDatabase()
-                .getRefs()
-                .stream()
+            rp.getRepository().getRefDatabase().getRefs().stream()
                 .collect(toMap(Ref::getName, x -> x));
       } catch (ServiceMayNotContinueException e) {
         throw e;
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 e74e1a2..6ffad7b 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -31,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;
@@ -50,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;
@@ -64,6 +64,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -92,10 +93,11 @@
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.SetHashtagsOp;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
@@ -111,7 +113,6 @@
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.server.git.receive.ResultChangeIds.Key;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.RefOperationValidationException;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
@@ -121,6 +122,7 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -157,7 +159,6 @@
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -174,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;
@@ -319,6 +323,7 @@
   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;
@@ -393,6 +398,7 @@
       SetHashtagsOp.Factory hashtagsFactory,
       SubmoduleOp.Factory subOpFactory,
       TagCache tagCache,
+      SetPrivateOp.Factory setPrivateOpFactory,
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
       @Assisted ReceivePack rp,
@@ -433,6 +439,7 @@
     this.subOpFactory = subOpFactory;
     this.tagCache = tagCache;
     this.projectConfigFactory = projectConfigFactory;
+    this.setPrivateOpFactory = setPrivateOpFactory;
 
     // Assisted injected fields.
     this.allRefsWatcher = allRefsWatcher;
@@ -681,7 +688,7 @@
     // Update superproject gitlinks if required.
     if (!branches.isEmpty()) {
       try (MergeOpRepoManager orm = ormProvider.get()) {
-        orm.setContext(TimeUtil.nowTs(), user);
+        orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
         SubmoduleOp op = subOpFactory.create(branches, orm);
         op.updateSuperProjects();
       } catch (SubmoduleException e) {
@@ -692,15 +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 =
-        replaceByChange
-            .values()
-            .stream()
+    // 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;
@@ -708,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) {
@@ -786,9 +831,15 @@
         RevWalk rw = new RevWalk(reader)) {
       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);
       }
 
@@ -807,11 +858,10 @@
         throw INSERT_EXCEPTION.apply(e);
       }
 
-      replaceByChange
-          .values()
-          .stream()
-          .forEach(req -> resultChangeIds.add(Key.REPLACED, req.ontoChange));
-      newChanges.stream().forEach(req -> resultChangeIds.add(Key.CREATED, req.changeId));
+      replaceByChange.values().stream()
+          .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.REPLACED, req.ontoChange));
+      newChanges.stream()
+          .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.CREATED, req.changeId));
 
       if (magicBranchCmd != null) {
         magicBranchCmd.setResult(OK);
@@ -832,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) {
@@ -847,7 +897,7 @@
         addError(e.getMessage());
         reject(magicBranchCmd, "conflict");
       } catch (RestApiException
-          | OrmException
+          | StorageException
           | UpdateException
           | IOException
           | ConfigInvalidException
@@ -1385,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",
@@ -1515,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;
@@ -1583,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;
@@ -1800,7 +1842,7 @@
     if (magicBranch.deprecatedTopicSeen) {
       messages.add(
           new ValidationMessage(
-              "WARNING: deprecated topic syntax. Use %topic=TOPIC instead", false));
+              "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false));
       logger.atInfo().log("deprecated topic push seen for project %s", project.getName());
     }
 
@@ -1896,7 +1938,7 @@
       logger.atSevere().withCause(e).log("Change not found %s", changeId);
       reject(cmd, "change " + changeId + " not found");
       return;
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Cannot lookup existing change %s", changeId);
       reject(cmd, "database error");
       return;
@@ -1931,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(
@@ -2195,7 +2237,7 @@
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
       logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
       return Collections.emptyList();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
       reject(magicBranch.cmd, "database error");
       return Collections.emptyList();
@@ -2225,14 +2267,14 @@
         update.groups = ImmutableList.copyOf((groups.get(update.commit)));
       }
       logger.atFine().log("Finished updating groups from GroupCollector");
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Error collecting groups for changes");
       reject(magicBranch.cmd, "internal server error");
     }
     return newChanges;
   }
 
-  private boolean foundInExistingRef(Collection<Ref> existingRefs) throws OrmException {
+  private boolean foundInExistingRef(Collection<Ref> existingRefs) {
     for (Ref ref : existingRefs) {
       ChangeNotes notes =
           notesFactory.create(project.getNameKey(), Change.Id.fromRef(ref.getName()));
@@ -2352,11 +2394,11 @@
     }
   }
 
-  private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) throws OrmException {
+  private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) {
     return new ChangeLookup(c, key, queryProvider.get().byBranchKey(magicBranch.dest, key));
   }
 
-  private ChangeLookup lookupByCommit(RevCommit c) throws OrmException {
+  private ChangeLookup lookupByCommit(RevCommit c) {
     return new ChangeLookup(
         c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
   }
@@ -2439,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));
@@ -2484,7 +2525,7 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
     for (CreateRequest r : create) {
@@ -2517,7 +2558,7 @@
           req.validateNewPatchSet();
         }
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atSevere().withCause(err).log(
           "Cannot read database before replacement for project %s", project.getName());
       rejectRemainingRequests(replaceByChange.values(), "internal server error");
@@ -2541,7 +2582,7 @@
     }
   }
 
-  private void readChangesForReplace() throws OrmException {
+  private void readChangesForReplace() {
     Collection<ChangeNotes> allNotes =
         notesFactory.create(
             replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
@@ -2556,6 +2597,7 @@
     final ObjectId newCommitId;
     final ReceiveCommand inputCommand;
     final boolean checkMergedInto;
+    RevCommit revCommit;
     ChangeNotes notes;
     BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet.Id psId;
@@ -2573,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 {
@@ -2599,10 +2646,9 @@
      *
      * @return whether the new commit is valid
      * @throws IOException
-     * @throws OrmException
      * @throws PermissionBackendException
      */
-    boolean validateNewPatchSet() throws IOException, OrmException, PermissionBackendException {
+    boolean validateNewPatchSet() throws IOException, PermissionBackendException {
       if (!validateNewPatchSetNoteDb()) {
         return false;
       }
@@ -2623,8 +2669,7 @@
       return true;
     }
 
-    boolean validateNewPatchSetForAutoClose()
-        throws IOException, OrmException, PermissionBackendException {
+    boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
       if (!validateNewPatchSetNoteDb()) {
         return false;
       }
@@ -2634,8 +2679,7 @@
     }
 
     /** Validates the new PS against permissions and notedb status. */
-    private boolean validateNewPatchSetNoteDb()
-        throws IOException, OrmException, PermissionBackendException {
+    private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
       if (notes == null) {
         reject(inputCommand, "change " + ontoChange + " not found");
         return false;
@@ -2663,7 +2707,7 @@
         return false;
       }
 
-      if (change.getStatus().isClosed()) {
+      if (change.isClosed()) {
         reject(inputCommand, "change " + ontoChange + " closed");
         return false;
       } else if (revisions.containsKey(newCommit)) {
@@ -2798,7 +2842,7 @@
     }
 
     /** Updates 'this' to add a new patchset. */
-    private void newPatchSet() throws IOException, OrmException {
+    private void newPatchSet() throws IOException {
       RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
       psId =
           ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs(), notes.getChange().currentPatchSetId());
@@ -2862,7 +2906,7 @@
           psId.getParentKey(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               PatchSet ps = psUtil.get(ctx.getNotes(), psId);
               List<String> oldGroups = ps.getGroups();
               if (oldGroups == null) {
@@ -3123,6 +3167,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));
@@ -3155,6 +3200,7 @@
                   continue;
                 }
                 req.addOps(bu, null);
+                bu.addOp(id, setPrivateOpFactory.create(false, null));
                 bu.addOp(
                     id,
                     mergedByPushOpFactory
@@ -3168,7 +3214,7 @@
                   "Auto-closing %s changes with existing patch sets and %s with new patch sets",
                   existingPatchSets, newPatchSets);
               bu.execute();
-            } catch (IOException | OrmException | PermissionBackendException e) {
+            } catch (IOException | StorageException | PermissionBackendException e) {
               logger.atSevere().withCause(e).log("Failed to auto-close changes");
               return null;
             }
@@ -3176,7 +3222,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;
           },
@@ -3192,7 +3238,7 @@
     }
   }
 
-  private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) throws OrmException {
+  private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) {
     try {
       return Optional.of(notesFactory.createChecked(project.getNameKey(), changeId));
     } catch (NoSuchChangeException e) {
@@ -3200,18 +3246,17 @@
     }
   }
 
-  private <T> T executeIndexQuery(Action<T> action) throws OrmException {
+  private <T> T executeIndexQuery(Action<T> action) {
     try {
-      return retryHelper.execute(ActionType.INDEX_QUERY, action, OrmException.class::isInstance);
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY, action, StorageException.class::isInstance);
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
-  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(Branch.NameKey branch)
-      throws OrmException {
+  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(Branch.NameKey branch) {
     Map<Change.Key, ChangeNotes> r = new HashMap<>();
     for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
       try {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 8cbcc88..9aa21af 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.util.Collections;
 import java.util.Map;
@@ -118,7 +118,7 @@
         }
       }
       return r;
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);
       return Collections.emptySet();
     }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0f68ba5..b15035a 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;
@@ -52,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;
@@ -74,7 +74,6 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.util.Providers;
@@ -242,11 +241,10 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     notes = ctx.getNotes();
     Change change = notes.getChange();
-    if (change == null || change.getStatus().isClosed()) {
+    if (change == null || change.isClosed()) {
       rejectMessage = CHANGE_IS_CLOSED;
       return false;
     }
@@ -364,13 +362,9 @@
       inputs =
           Streams.concat(
               inputs,
-              magicBranch
-                  .getCombinedReviewers(fromFooters)
-                  .stream()
+              magicBranch.getCombinedReviewers(fromFooters).stream()
                   .map(r -> newAddReviewerInput(r, ReviewerState.REVIEWER)),
-              magicBranch
-                  .getCombinedCcs(fromFooters)
-                  .stream()
+              magicBranch.getCombinedCcs(fromFooters).stream()
                   .map(r -> newAddReviewerInput(r, ReviewerState.CC)));
     }
     return inputs.collect(toImmutableList());
@@ -391,7 +385,7 @@
   }
 
   private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
-      throws OrmException, IOException {
+      throws IOException {
     String approvalMessage =
         ApprovalsUtil.renderMessageWithApprovals(
             patchSetId.get(), approvals, scanLabels(ctx, approvals));
@@ -445,7 +439,7 @@
   }
 
   private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals)
-      throws OrmException, IOException {
+      throws IOException {
     Map<String, PatchSetApproval> current = new HashMap<>();
     // We optimize here and only retrieve current when approvals provided
     if (!approvals.isEmpty()) {
@@ -489,8 +483,7 @@
     }
   }
 
-  private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress)
-      throws OrmException {
+  private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress) {
     List<Comment> comments =
         commentsUtil.draftByChangeAuthor(ctx.getNotes(), ctx.getUser().getAccountId());
     publishCommentUtil.publish(
@@ -512,13 +505,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(),
@@ -555,16 +546,11 @@
         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(),
-                    reviewerAdditions
-                        .flattenResults(AddReviewersOp.Result::addedReviewers)
-                        .stream()
+                    reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
                         .map(PatchSetApproval::getAccountId))
                 .collect(toImmutableSet()));
         cm.addExtraCC(
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index e9fe562..08870d3 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -114,9 +114,7 @@
     accountConfig.load(allUsersName, rw, commit);
     if (messages != null) {
       messages.addAll(
-          accountConfig
-              .getValidationErrors()
-              .stream()
+          accountConfig.getValidationErrors().stream()
               .map(ValidationError::getMessage)
               .collect(toSet()));
     }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index e3dfa75..d42fbcf 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -21,18 +21,16 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -89,7 +87,7 @@
   @Singleton
   public static class Factory {
     private final PersonIdent gerritIdent;
-    private final UrlFormatter urlFormatter;
+    private final DynamicItem<UrlFormatter> urlFormatter;
     private final PluginSetContext<CommitValidationListener> pluginValidators;
     private final GitRepositoryManager repoManager;
     private final AllUsersName allUsers;
@@ -103,7 +101,7 @@
     @Inject
     Factory(
         @GerritPersonIdent PersonIdent gerritIdent,
-        UrlFormatter urlFormatter,
+        DynamicItem<UrlFormatter> urlFormatter,
         @GerritServerConfig Config cfg,
         PluginSetContext<CommitValidationListener> pluginValidators,
         GitRepositoryManager repoManager,
@@ -143,11 +141,16 @@
               new UploadMergesPermissionValidator(perm),
               new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
-              new AuthorUploaderValidator(user, perm, urlFormatter),
-              new CommitterUploaderValidator(user, perm, urlFormatter),
+              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
+              new CommitterUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
-                  projectState, user, urlFormatter, installCommitMsgHookCommand, sshInfo, change),
+                  projectState,
+                  user,
+                  urlFormatter.get(),
+                  installCommitMsgHookCommand,
+                  sshInfo,
+                  change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators),
@@ -158,7 +161,7 @@
 
     public CommitValidators forGerritCommits(
         PermissionBackend.ForProject forProject,
-        NameKey branch,
+        Branch.NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         RevWalk rw,
@@ -171,10 +174,15 @@
               new UploadMergesPermissionValidator(perm),
               new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
-              new AuthorUploaderValidator(user, perm, urlFormatter),
+              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.getParentKey())),
               new ChangeIdValidator(
-                  projectState, user, urlFormatter, installCommitMsgHookCommand, sshInfo, change),
+                  projectState,
+                  user,
+                  urlFormatter.get(),
+                  installCommitMsgHookCommand,
+                  sshInfo,
+                  change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -203,8 +211,8 @@
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
               new ProjectStateValidationListener(projectCache.checkedGet(branch.getParentKey())),
-              new AuthorUploaderValidator(user, perm, urlFormatter),
-              new CommitterUploaderValidator(user, perm, urlFormatter)));
+              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
+              new CommitterUploaderValidator(user, perm, urlFormatter.get())));
     }
   }
 
@@ -236,6 +244,7 @@
     private static final String MISSING_CHANGE_ID_MSG = "missing Change-Id in message footer";
     private static final String MISSING_SUBJECT_MSG =
         "missing subject; Change-Id must be in message footer";
+    private static final String CHANGE_ID_ABOVE_FOOTER_MSG = "Change-Id must be in message footer";
     private static final String MULTIPLE_CHANGE_ID_MSG =
         "multiple Change-Id lines in message footer";
     private static final String INVALID_CHANGE_ID_MSG =
@@ -285,8 +294,20 @@
             && CHANGE_ID.matcher(shortMsg.substring(CHANGE_ID_PREFIX.length()).trim()).matches()) {
           throw new CommitValidationException(MISSING_SUBJECT_MSG);
         }
+        if (commit.getFullMessage().contains("\n" + CHANGE_ID_PREFIX)) {
+          messages.add(
+              new CommitValidationMessage(
+                  CHANGE_ID_ABOVE_FOOTER_MSG
+                      + "\n"
+                      + "\n"
+                      + "Hint: run\n"
+                      + "  git commit --amend\n"
+                      + "and move 'Change-Id: Ixxx..' to the bottom on a separate line\n",
+                  Type.ERROR));
+          throw new CommitValidationException(CHANGE_ID_ABOVE_FOOTER_MSG, messages);
+        }
         if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
-          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG, commit));
+          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
           throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
         }
       } else if (idList.size() > 1) {
@@ -296,7 +317,7 @@
         // Reject Change-Ids with wrong format and invalid placeholder ID from
         // Egit (I0000000000000000000000000000000000000000).
         if (!CHANGE_ID.matcher(v).matches() || v.matches("^I00*$")) {
-          messages.add(getMissingChangeIdErrorMsg(INVALID_CHANGE_ID_MSG, receiveEvent.commit));
+          messages.add(getMissingChangeIdErrorMsg(INVALID_CHANGE_ID_MSG));
           throw new CommitValidationException(INVALID_CHANGE_ID_MSG, messages);
         }
         if (change != null && !v.equals(change.getKey().get())) {
@@ -312,32 +333,17 @@
           || NEW_PATCHSET_PATTERN.matcher(event.command.getRefName()).matches();
     }
 
-    private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg, RevCommit c) {
-      StringBuilder sb = new StringBuilder();
-      sb.append(errMsg).append("\n");
-
-      boolean hinted = false;
-      if (c.getFullMessage().contains(CHANGE_ID_PREFIX)) {
-        String lastLine = Iterables.getLast(Splitter.on('\n').split(c.getFullMessage()), "");
-        if (!lastLine.contains(CHANGE_ID_PREFIX)) {
-          hinted = true;
-          sb.append("\n")
-              .append("Hint: run\n")
-              .append("  git commit --amend\n")
-              .append("and move 'Change-Id: Ixxx..' to the bottom on a separate line\n");
-        }
-      }
-
-      // Print only one hint to avoid overwhelming the user.
-      if (!hinted) {
-        sb.append("\nHint: to automatically insert a Change-Id, install the hook:\n")
-            .append(getCommitMessageHookInstallationHint())
-            .append("\n")
-            .append("and then amend the commit:\n")
-            .append("  git commit --amend\n")
-            .append("Finally, push your changes again\n");
-      }
-      return new CommitValidationMessage(sb.toString(), Type.ERROR);
+    private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg) {
+      return new CommitValidationMessage(
+          errMsg
+              + "\n"
+              + "\nHint: to automatically insert a Change-Id, install the hook:\n"
+              + getCommitMessageHookInstallationHint()
+              + "\n"
+              + "and then amend the commit:\n"
+              + "  git commit --amend --no-edit\n"
+              + "Finally, push your changes again\n",
+          Type.ERROR);
     }
 
     private String getCommitMessageHookInstallationHint() {
@@ -483,7 +489,8 @@
       List<CommitValidationMessage> messages = new ArrayList<>();
       try {
         commitValidationListeners.runEach(
-            l -> l.onCommitReceived(receiveEvent), CommitValidationException.class);
+            l -> messages.addAll(l.onCommitReceived(receiveEvent)),
+            CommitValidationException.class);
       } catch (CommitValidationException e) {
         messages.addAll(e.getMessages());
         throw new CommitValidationException(e.getMessage(), messages);
@@ -695,8 +702,7 @@
           List<ConsistencyProblemInfo> problems =
               externalIdsConsistencyChecker.check(receiveEvent.commit);
           List<CommitValidationMessage> msgs =
-              problems
-                  .stream()
+              problems.stream()
                   .map(
                       p ->
                           new CommitValidationMessage(
@@ -762,8 +768,7 @@
         if (!errorMessages.isEmpty()) {
           throw new CommitValidationException(
               "invalid account configuration",
-              errorMessages
-                  .stream()
+              errorMessages.stream()
                   .map(m -> new CommitValidationMessage(m, Type.ERROR))
                   .collect(toList()));
         }
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 1dd48f1..e7e021b 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
@@ -43,7 +44,6 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
@@ -301,7 +301,7 @@
         if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
           return;
         }
-      } catch (IOException | OrmException e) {
+      } catch (StorageException e) {
         logger.atSevere().withCause(e).log("Cannot validate account update");
         throw new MergeValidationException("account validation unavailable");
       }
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 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/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
index dbbc3f6..2a9538d 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -125,8 +125,7 @@
   public synchronized void run() {
     try (Repository allUsers = repoManager.openRepository(allUsersName)) {
       ImmutableSet<AccountGroup.UUID> newGroupUuids =
-          GroupNameNotes.loadAllGroups(allUsers)
-              .stream()
+          GroupNameNotes.loadAllGroups(allUsers).stream()
               .map(GroupReference::getUUID)
               .collect(toImmutableSet());
       GroupIndexer groupIndexer = groupIndexerProvider.get();
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 66230ea..903b9c0 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -24,6 +24,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Arrays;
@@ -57,13 +57,13 @@
  * AccountGroup.UUID)} or {@link #loadForGroupSnapshot(Project.NameKey, Repository,
  * AccountGroup.UUID, ObjectId)}.
  *
- * <p><strong>Note: </strong>Any modification (group creation or update) only becomes permanent (and
+ * <p><strong>Note:</strong> Any modification (group creation or update) only becomes permanent (and
  * hence written to NoteDb) if {@link #commit(MetaDataUpdate)} is called.
  *
- * <p><strong>Warning: </strong>This class is a low-level API for groups in NoteDb. Most code which
+ * <p><strong>Warning:</strong> This class is a low-level API for groups in NoteDb. Most code which
  * deals with internal Gerrit groups should use {@link Groups} or {@link GroupsUpdate} instead.
  *
- * <p><em>Internal details</em>
+ * <h2>Internal details</h2>
  *
  * <p>Each group is represented by a commit on a branch as defined by {@link
  * RefNames#refsGroups(AccountGroup.UUID)}. Previous versions of the group exist as older commits on
@@ -99,7 +99,7 @@
    * {@link #setGroupUpdate(InternalGroupUpdate, AuditLogFormatter)} on the returned {@code
    * GroupConfig}.
    *
-   * <p><strong>Note: </strong>The returned {@code GroupConfig} has to be committed via {@link
+   * <p><strong>Note:</strong> The returned {@code GroupConfig} has to be committed via {@link
    * #commit(MetaDataUpdate)} in order to create the group for real.
    *
    * @param projectName the name of the project which holds the NoteDb commits for groups
@@ -110,11 +110,11 @@
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException if a group with the same UUID already exists but can't be read
    *     due to an invalid format
-   * @throws OrmDuplicateKeyException if a group with the same UUID already exists
+   * @throws DuplicateKeyException if a group with the same UUID already exists
    */
   public static GroupConfig createForNewGroup(
       Project.NameKey projectName, Repository repository, InternalGroupCreation groupCreation)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     GroupConfig groupConfig = new GroupConfig(groupCreation.getGroupUUID());
     groupConfig.load(projectName, repository);
     groupConfig.setGroupCreation(groupCreation);
@@ -216,7 +216,7 @@
    * <p>If the group is newly created, the {@code InternalGroupUpdate} can be used to specify
    * optional properties.
    *
-   * <p><strong>Note: </strong>This method doesn't perform the update. It only contains the
+   * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
    * instructions for the update. To apply the update for real and write the result back to NoteDb,
    * call {@link #commit(MetaDataUpdate)} on this {@code GroupConfig}.
    *
@@ -233,7 +233,7 @@
   /**
    * Allows the new name of a group to be empty during creation or update.
    *
-   * <p><strong>Note: </strong>This method exists only to support the migration of legacy groups
+   * <p><strong>Note:</strong> This method exists only to support the migration of legacy groups
    * which don't always necessarily have a name. Nowadays, we enforce that groups always have names.
    * When we remove the migration code, we can probably remove this method as well.
    */
@@ -241,11 +241,10 @@
     this.allowSaveEmptyName = true;
   }
 
-  private void setGroupCreation(InternalGroupCreation groupCreation)
-      throws OrmDuplicateKeyException {
+  private void setGroupCreation(InternalGroupCreation groupCreation) throws DuplicateKeyException {
     checkLoaded();
     if (loadedGroup.isPresent()) {
-      throw new OrmDuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
+      throw new DuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
     }
 
     this.groupCreation = Optional.of(groupCreation);
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java b/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
index 62cc20d..5627154 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
@@ -107,13 +107,11 @@
     Function<T, String> toString = element -> toParsableString.apply(auditLogFormatter, element);
 
     Stream<String> removedElements =
-        Sets.difference(oldElements, newElements)
-            .stream()
+        Sets.difference(oldElements, newElements).stream()
             .map(toString)
             .map((removalFooterKey.getName() + ": ")::concat);
     Stream<String> addedElements =
-        Sets.difference(newElements, oldElements)
-            .stream()
+        Sets.difference(newElements, oldElements).stream()
             .map(toString)
             .map((additionFooterKey.getName() + ": ")::concat);
     return Stream.concat(removedElements, addedElements);
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
index eff3458..f7a104d 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
@@ -25,7 +25,7 @@
  *
  * <p>Each property knows how to read and write its value from/to a JGit {@link Config} file.
  *
- * <p><strong>Warning: </strong>This class is a low-level API for properties of groups in NoteDb. It
+ * <p><strong>Warning:</strong> This class is a low-level API for properties of groups in NoteDb. It
  * may only be used by {@link GroupConfig}. Other classes should use {@link InternalGroupUpdate} to
  * modify the properties of a group.
  */
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index 6c21dc4..eda7153 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -29,11 +29,11 @@
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
@@ -114,7 +114,7 @@
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException if the note for the specified group doesn't exist or is in an
    *     invalid state
-   * @throws OrmDuplicateKeyException if a group with the new name already exists
+   * @throws DuplicateKeyException if a group with the new name already exists
    */
   public static GroupNameNotes forRename(
       Project.NameKey projectName,
@@ -122,7 +122,7 @@
       AccountGroup.UUID groupUuid,
       AccountGroup.NameKey oldName,
       AccountGroup.NameKey newName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     requireNonNull(oldName);
     requireNonNull(newName);
 
@@ -146,14 +146,14 @@
    * @return an instance of {@code GroupNameNotes} configured for a specific group creation
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException in no case so far
-   * @throws OrmDuplicateKeyException if a group with the new name already exists
+   * @throws DuplicateKeyException if a group with the new name already exists
    */
   public static GroupNameNotes forNewGroup(
       Project.NameKey projectName,
       Repository repository,
       AccountGroup.UUID groupUuid,
       AccountGroup.NameKey groupName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     requireNonNull(groupName);
 
     GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
@@ -295,8 +295,7 @@
   private static ImmutableBiMap<AccountGroup.UUID, String> toBiMap(
       Collection<GroupReference> groupReferences) {
     try {
-      return groupReferences
-          .stream()
+      return groupReferences.stream()
           .collect(toImmutableBiMap(GroupReference::getUUID, GroupReference::getName));
     } catch (IllegalArgumentException e) {
       throw new IllegalArgumentException(UNIQUE_REF_ERROR, e);
@@ -364,9 +363,9 @@
     }
   }
 
-  private void ensureNewNameIsNotUsed() throws OrmDuplicateKeyException {
+  private void ensureNewNameIsNotUsed() throws DuplicateKeyException {
     if (newGroupName.isPresent() && nameConflicting) {
-      throw new OrmDuplicateKeyException(
+      throw new DuplicateKeyException(
           String.format("Name '%s' is already used", newGroupName.get().get()));
     }
   }
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index f2289d4..37de011 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -124,9 +124,7 @@
           getGroupFromNoteDb(allUsersName, allUsersRepo, internalGroup.getUUID());
       group.map(InternalGroup::getSubgroups).ifPresent(allSubgroups::addAll);
     }
-    return allSubgroups
-        .build()
-        .stream()
+    return allSubgroups.build().stream()
         .filter(groupUuid -> !AccountGroup.isInternalGroup(groupUuid));
   }
 
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index d397b0d..b450ab8 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -19,8 +19,10 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -40,12 +42,13 @@
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.update.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,15 +78,28 @@
      * modifications executed by it. For NoteDb, this identity is used as author and committer for
      * all related commits.
      *
-     * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
-     * correct annotation on the provider of a {@code GroupsUpdate} instead.
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@code GroupsUpdate}
+     * instead.
      *
-     * @param currentUser the user to which modifications should be attributed, or {@code null} if
-     *     the Gerrit server identity should be used
+     * @param currentUser the user to which modifications should be attributed
      */
-    GroupsUpdate create(@Nullable IdentifiedUser currentUser);
+    GroupsUpdate create(IdentifiedUser currentUser);
+
+    /**
+     * Creates a {@code GroupsUpdate} which uses the server identity to mark database modifications
+     * executed by it. For NoteDb, this identity is used as author and committer for all related
+     * commits.
+     *
+     * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+     * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@code
+     * GroupsUpdate} instead.
+     */
+    GroupsUpdate createWithServerIdent();
   }
 
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final GroupCache groupCache;
@@ -91,14 +107,48 @@
   private final Provider<GroupIndexer> indexer;
   private final GroupAuditService groupAuditService;
   private final RenameGroupOp.Factory renameGroupOpFactory;
-  @Nullable private final IdentifiedUser currentUser;
+  private final Optional<IdentifiedUser> currentUser;
   private final AuditLogFormatter auditLogFormatter;
   private final PersonIdent authorIdent;
   private final MetaDataUpdateFactory metaDataUpdateFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final RetryHelper retryHelper;
 
-  @Inject
+  @AssistedInject
+  GroupsUpdate(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      GroupBackend groupBackend,
+      GroupCache groupCache,
+      GroupIncludeCache groupIncludeCache,
+      Provider<GroupIndexer> indexer,
+      GroupAuditService auditService,
+      AccountCache accountCache,
+      RenameGroupOp.Factory renameGroupOpFactory,
+      @GerritServerId String serverId,
+      @GerritPersonIdent PersonIdent serverIdent,
+      MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+      GitReferenceUpdated gitRefUpdated,
+      RetryHelper retryHelper) {
+    this(
+        repoManager,
+        allUsersName,
+        groupBackend,
+        groupCache,
+        groupIncludeCache,
+        indexer,
+        auditService,
+        accountCache,
+        renameGroupOpFactory,
+        serverId,
+        serverIdent,
+        metaDataUpdateInternalFactory,
+        gitRefUpdated,
+        retryHelper,
+        Optional.empty());
+  }
+
+  @AssistedInject
   GroupsUpdate(
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
@@ -114,7 +164,41 @@
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
       GitReferenceUpdated gitRefUpdated,
       RetryHelper retryHelper,
-      @Assisted @Nullable IdentifiedUser currentUser) {
+      @Assisted IdentifiedUser currentUser) {
+    this(
+        repoManager,
+        allUsersName,
+        groupBackend,
+        groupCache,
+        groupIncludeCache,
+        indexer,
+        auditService,
+        accountCache,
+        renameGroupOpFactory,
+        serverId,
+        serverIdent,
+        metaDataUpdateInternalFactory,
+        gitRefUpdated,
+        retryHelper,
+        Optional.of(currentUser));
+  }
+
+  private GroupsUpdate(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      GroupBackend groupBackend,
+      GroupCache groupCache,
+      GroupIncludeCache groupIncludeCache,
+      Provider<GroupIndexer> indexer,
+      GroupAuditService auditService,
+      AccountCache accountCache,
+      RenameGroupOp.Factory renameGroupOpFactory,
+      @GerritServerId String serverId,
+      @GerritPersonIdent PersonIdent serverIdent,
+      MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+      GitReferenceUpdated gitRefUpdated,
+      RetryHelper retryHelper,
+      Optional<IdentifiedUser> currentUser) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.groupCache = groupCache;
@@ -135,7 +219,7 @@
 
   private static MetaDataUpdateFactory getMetaDataUpdateFactory(
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
-      @Nullable IdentifiedUser currentUser,
+      Optional<IdentifiedUser> currentUser,
       PersonIdent serverIdent,
       AuditLogFormatter auditLogFormatter) {
     return (projectName, repository, batchRefUpdate) -> {
@@ -143,10 +227,10 @@
           metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate);
       metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
       PersonIdent authorIdent;
-      if (currentUser != null) {
-        metaDataUpdate.setAuthor(currentUser);
+      if (currentUser.isPresent()) {
+        metaDataUpdate.setAuthor(currentUser.get());
         authorIdent =
-            auditLogFormatter.getParsableAuthorIdent(currentUser.getAccount(), serverIdent);
+            auditLogFormatter.getParsableAuthorIdent(currentUser.get().getAccount(), serverIdent);
       } else {
         authorIdent = serverIdent;
       }
@@ -156,8 +240,8 @@
   }
 
   private static PersonIdent getAuthorIdent(
-      PersonIdent serverIdent, @Nullable IdentifiedUser currentUser) {
-    return currentUser != null ? createPersonIdent(serverIdent, currentUser) : serverIdent;
+      PersonIdent serverIdent, Optional<IdentifiedUser> currentUser) {
+    return currentUser.map(user -> createPersonIdent(serverIdent, user)).orElse(serverIdent);
   }
 
   private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -172,17 +256,21 @@
    * @param groupUpdate an {@code InternalGroupUpdate} which specifies optional properties of the
    *     group. If this {@code InternalGroupUpdate} updates a property which was already specified
    *     by the {@code InternalGroupCreation}, the value of this {@code InternalGroupUpdate} wins.
-   * @throws OrmDuplicateKeyException if a group with the chosen name already exists
+   * @throws DuplicateKeyException if a group with the chosen name already exists
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @return the created {@code InternalGroup}
    */
   public InternalGroup createGroup(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmDuplicateKeyException, IOException, ConfigInvalidException {
-    InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupUpdate);
-    updateCachesOnGroupCreation(createdGroup);
-    dispatchAuditEventsOnGroupCreation(createdGroup);
-    return createdGroup;
+      throws DuplicateKeyException, IOException, ConfigInvalidException {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Creating group '%s'", groupUpdate.getName().orElseGet(groupCreation::getNameKey))) {
+      InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupUpdate);
+      evictCachesOnGroupCreation(createdGroup);
+      dispatchAuditEventsOnGroupCreation(createdGroup);
+      return createdGroup;
+    }
   }
 
   /**
@@ -191,27 +279,29 @@
    * @param groupUuid the UUID of the group to update
    * @param groupUpdate an {@code InternalGroupUpdate} which indicates the desired updates on the
    *     group
-   * @throws OrmDuplicateKeyException if the new name of the group is used by another group
+   * @throws DuplicateKeyException if the new name of the group is used by another group
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
   public void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmDuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
-    Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
-    if (!updatedOn.isPresent()) {
-      updatedOn = Optional.of(TimeUtil.nowTs());
-      groupUpdate = groupUpdate.toBuilder().setUpdatedOn(updatedOn.get()).build();
-    }
+      throws DuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
+    try (TraceTimer timer = TraceContext.newTimer("Updating group %s", groupUuid)) {
+      Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
+      if (!updatedOn.isPresent()) {
+        updatedOn = Optional.of(TimeUtil.nowTs());
+        groupUpdate = groupUpdate.toBuilder().setUpdatedOn(updatedOn.get()).build();
+      }
 
-    UpdateResult result = updateGroupInNoteDbWithRetry(groupUuid, groupUpdate);
-    updateNameInProjectConfigsIfNecessary(result);
-    updateCachesOnGroupUpdate(result);
-    dispatchAuditEventsOnGroupUpdate(result, updatedOn.get());
+      UpdateResult result = updateGroupInNoteDbWithRetry(groupUuid, groupUpdate);
+      updateNameInProjectConfigsIfNecessary(result);
+      evictCachesOnGroupUpdate(result);
+      dispatchAuditEventsOnGroupUpdate(result, updatedOn.get());
+    }
   }
 
   private InternalGroup createGroupInNoteDbWithRetry(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     try {
       return retryHelper.execute(
           RetryHelper.ActionType.GROUP_UPDATE,
@@ -221,7 +311,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmDuplicateKeyException.class);
+      Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
       throw new IOException(e);
     }
   }
@@ -229,7 +319,7 @@
   @VisibleForTesting
   public InternalGroup createGroupInNoteDb(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException {
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
       GroupNameNotes groupNameNotes =
@@ -251,7 +341,7 @@
 
   private UpdateResult updateGroupInNoteDbWithRetry(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
     try {
       return retryHelper.execute(
           RetryHelper.ActionType.GROUP_UPDATE,
@@ -261,7 +351,7 @@
       Throwables.throwIfUnchecked(e);
       Throwables.throwIfInstanceOf(e, IOException.class);
       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, OrmDuplicateKeyException.class);
+      Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
       Throwables.throwIfInstanceOf(e, NoSuchGroupException.class);
       throw new IOException(e);
     }
@@ -270,7 +360,7 @@
   @VisibleForTesting
   public UpdateResult updateGroupInNoteDb(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
+      throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid);
       groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
@@ -342,10 +432,11 @@
 
     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 {
+  private void evictCachesOnGroupCreation(InternalGroup createdGroup) {
+    logger.atFine().log("evict caches on creation of group %s", createdGroup.getGroupUUID());
     // By UUID is used for the index and hence should be evicted before refreshing the index.
     groupCache.evict(createdGroup.getGroupUUID());
     indexer.get().index(createdGroup.getGroupUUID());
@@ -357,7 +448,8 @@
     createdGroup.getSubgroups().forEach(groupIncludeCache::evictParentGroupsOf);
   }
 
-  private void updateCachesOnGroupUpdate(UpdateResult result) throws IOException {
+  private void evictCachesOnGroupUpdate(UpdateResult result) {
+    logger.atFine().log("evict caches on update of group %s", result.getGroupUuid());
     // By UUID is used for the index and hence should be evicted before refreshing the index.
     groupCache.evict(result.getGroupUuid());
     indexer.get().index(result.getGroupUuid());
@@ -390,20 +482,20 @@
   }
 
   private void dispatchAuditEventsOnGroupCreation(InternalGroup createdGroup) {
-    if (currentUser == null) {
+    if (!currentUser.isPresent()) {
       return;
     }
 
     if (!createdGroup.getMembers().isEmpty()) {
       groupAuditService.dispatchAddMembers(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getMembers(),
           createdGroup.getCreatedOn());
     }
     if (!createdGroup.getSubgroups().isEmpty()) {
       groupAuditService.dispatchAddSubgroups(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getSubgroups(),
           createdGroup.getCreatedOn());
@@ -411,25 +503,34 @@
   }
 
   private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Timestamp updatedOn) {
-    if (currentUser == null) {
+    if (!currentUser.isPresent()) {
       return;
     }
 
     if (!result.getAddedMembers().isEmpty()) {
       groupAuditService.dispatchAddMembers(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getAddedMembers(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getAddedMembers(),
+          updatedOn);
     }
     if (!result.getDeletedMembers().isEmpty()) {
       groupAuditService.dispatchDeleteMembers(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getDeletedMembers(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getDeletedMembers(),
+          updatedOn);
     }
     if (!result.getAddedSubgroups().isEmpty()) {
       groupAuditService.dispatchAddSubgroups(
-          currentUser.getAccountId(), result.getGroupUuid(), result.getAddedSubgroups(), updatedOn);
+          currentUser.get().getAccountId(),
+          result.getGroupUuid(),
+          result.getAddedSubgroups(),
+          updatedOn);
     }
     if (!result.getDeletedSubgroups().isEmpty()) {
       groupAuditService.dispatchDeleteSubgroups(
-          currentUser.getAccountId(),
+          currentUser.get().getAccountId(),
           result.getGroupUuid(),
           result.getDeletedSubgroups(),
           updatedOn);
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index f0ab638..2f91394 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -23,7 +23,6 @@
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
@@ -32,7 +31,11 @@
 public class InternalGroupSubject extends Subject<InternalGroupSubject, InternalGroup> {
 
   public static InternalGroupSubject assertThat(InternalGroup group) {
-    return assertAbout(InternalGroupSubject::new).that(group);
+    return assertAbout(internalGroups()).that(group);
+  }
+
+  public static Subject.Factory<InternalGroupSubject, InternalGroup> internalGroups() {
+    return InternalGroupSubject::new;
   }
 
   private InternalGroupSubject(FailureMetadata metadata, InternalGroup actual) {
@@ -42,66 +45,66 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getGroupUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getGroupUUID());
   }
 
   public ComparableSubject<?, AccountGroup.NameKey> nameKey() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getNameKey()).named("nameKey");
+    return check("nameKey()").that(group.getNameKey());
   }
 
   public StringSubject name() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 
-  public DefaultSubject id() {
+  public Subject<DefaultSubject, Object> id() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getId()).named("id");
+    return check("id()").that(group.getId());
   }
 
   public StringSubject description() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getDescription()).named("description");
+    return check("description()").that(group.getDescription());
   }
 
   public ComparableSubject<?, AccountGroup.UUID> ownerGroupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getOwnerGroupUUID()).named("ownerGroupUuid");
+    return check("ownerGroupUuid()").that(group.getOwnerGroupUUID());
   }
 
   public BooleanSubject visibleToAll() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.isVisibleToAll()).named("visibleToAll");
+    return check("visibleToAll()").that(group.isVisibleToAll());
   }
 
   public ComparableSubject<?, Timestamp> createdOn() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getCreatedOn()).named("createdOn");
+    return check("createdOn()").that(group.getCreatedOn());
   }
 
   public IterableSubject members() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getMembers()).named("members");
+    return check("members()").that(group.getMembers());
   }
 
   public IterableSubject subgroups() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getSubgroups()).named("subgroups");
+    return check("subgroups()").that(group.getSubgroups());
   }
 
   public ComparableSubject<?, ObjectId> refState() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getRefState()).named("refState");
+    return check("refState()").that(group.getRefState());
   }
 }
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index e7bdfea..4b5cd49 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -18,10 +18,10 @@
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.server.CurrentUser;
@@ -31,41 +31,28 @@
 import com.google.gerrit.server.query.change.SingleGroupUser;
 import java.io.IOException;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public final class IndexUtils {
   public static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
       ImmutableMap.of("_", " ", ".", " ");
 
-  public static final Function<Exception, IOException> MAPPER =
-      in -> {
-        if (in instanceof IOException) {
-          return (IOException) in;
-        } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
-          return (IOException) in.getCause();
-        } else {
-          return new IOException(in);
-        }
-      };
-
-  public static void setReady(SitePaths sitePaths, String name, int version, boolean ready)
-      throws IOException {
+  public static void setReady(SitePaths sitePaths, String name, int version, boolean ready) {
     try {
       GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
       cfg.setReady(name, version, ready);
       cfg.save();
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
+    } catch (ConfigInvalidException | IOException e) {
+      throw new StorageException(e);
     }
   }
 
-  public static boolean getReady(SitePaths sitePaths, String name, int version) throws IOException {
+  public static boolean getReady(SitePaths sitePaths, String name, int version) {
     try {
       GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
       return cfg.getReady(name, version);
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
+    } catch (ConfigInvalidException | IOException e) {
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index 8a1776d..37677bdd 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -23,7 +23,6 @@
 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;
 
@@ -62,7 +61,7 @@
               try {
                 reindex();
                 ok = true;
-              } catch (IOException e) {
+              } catch (RuntimeException e) {
                 logger.atSevere().withCause(e).log(
                     "Online reindex of %s schema version %s failed", name, version(index));
               } finally {
@@ -91,7 +90,7 @@
     return i.getSchema().getVersion();
   }
 
-  private void reindex() throws IOException {
+  private void reindex() {
     listeners.runEach(listener -> listener.onStart(name, oldVersion, newVersion));
     index =
         requireNonNull(
@@ -120,11 +119,7 @@
   public void activateIndex() {
     indexes.setSearchIndex(index);
     logger.atInfo().log("Using %s schema version %s", name, version(index));
-    try {
-      index.markReady(true);
-    } catch (IOException e) {
-      logger.atWarning().log("Error activating new %s schema version %s", name, version(index));
-    }
+    index.markReady(true);
 
     List<I> toRemove = Lists.newArrayListWithExpectedSize(1);
     for (I i : indexes.getWriteIndexes()) {
@@ -133,12 +128,8 @@
       }
     }
     for (I i : toRemove) {
-      try {
-        i.markReady(false);
-        indexes.removeWriteIndex(version(i));
-      } catch (IOException e) {
-        logger.atWarning().log("Error deactivating old %s schema version %s", name, version(i));
-      }
+      i.markReady(false);
+      indexes.removeWriteIndex(version(i));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index 111991c..f67a41d 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -157,8 +157,7 @@
       storedOnly("external_id_state")
           .buildRepeatable(
               a ->
-                  a.getExternalIds()
-                      .stream()
+                  a.getExternalIds().stream()
                       .filter(e -> e.blobId() != null)
                       .map(ExternalId::toByteArray)
                       .collect(toSet()));
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java
index 91fa1d9..7f4f295 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index.account;
 
 import com.google.gerrit.reviewdb.client.Account;
-import java.io.IOException;
 
 public interface AccountIndexer {
 
@@ -24,7 +23,7 @@
    *
    * @param id account id to index.
    */
-  void index(Account.Id id) throws IOException;
+  void index(Account.Id id);
 
   /**
    * Synchronously reindex an account if it is stale.
@@ -32,5 +31,5 @@
    * @param id account id to index.
    * @return whether the account was reindexed
    */
-  boolean reindexIfStale(Account.Id id) throws IOException;
+  boolean reindexIfStale(Account.Id id);
 }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index f1b8591..1eaac7a 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.reviewdb.client.Account;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public void index(Account.Id id) throws IOException {
+  public void index(Account.Id id) {
     byIdCache.evict(id);
     Optional<AccountState> accountState = byIdCache.get(id);
 
@@ -104,10 +105,14 @@
   }
 
   @Override
-  public boolean reindexIfStale(Account.Id id) throws IOException {
-    if (stalenessChecker.isStale(id)) {
-      index(id);
-      return true;
+  public boolean reindexIfStale(Account.Id id) {
+    try {
+      if (stalenessChecker.isStale(id)) {
+        index(id);
+        return true;
+      }
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
index 18f44f0..8b9fa27 100644
--- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
+++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gwtorm.server.OrmException;
 
 public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState>
     implements DataSource<AccountState>, Matchable<AccountState> {
@@ -37,7 +36,7 @@
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     Predicate<AccountState> pred = getChild(0);
     checkState(
         pred.isMatchable(),
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 78e14e4..577c255 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -130,9 +130,7 @@
     // Estimate size based on IDs that show up in ref names. This is not perfect, since patch set
     // refs may exist for changes whose metadata was never successfully stored. But that's ok, as
     // the estimate is just used as a heuristic for sorting projects.
-    return repo.getRefDatabase()
-        .getRefsByPrefix(RefNames.REFS_CHANGES)
-        .stream()
+    return repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES).stream()
         .map(r -> Change.Id.fromRef(r.getName()))
         .filter(Objects::nonNull)
         .distinct()
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 9e8f111..394761b 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.index.FieldDef.storedOnly;
 import static com.google.gerrit.index.FieldDef.timestamp;
 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;
 
@@ -37,6 +38,7 @@
 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;
@@ -70,8 +72,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -152,13 +152,8 @@
       exact(ChangeQueryBuilder.FIELD_FILE)
           .buildRepeatable(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
 
-  public static Set<String> getFileParts(ChangeData cd) throws OrmException {
-    List<String> paths;
-    try {
-      paths = cd.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public static Set<String> getFileParts(ChangeData cd) {
+    List<String> paths = cd.currentFilePaths();
 
     Splitter s = Splitter.on('/').omitEmptyStrings();
     Set<String> r = new HashSet<>();
@@ -185,6 +180,89 @@
   public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
       exact(ChangeQueryBuilder.FIELD_FILEPART).buildRepeatable(ChangeField::getFileParts);
 
+  /** File extensions of each file modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> EXTENSION =
+      exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
+
+  public static Set<String> getExtensions(ChangeData cd) {
+    return extensions(cd).collect(toSet());
+  }
+
+  /**
+   * File extensions of each file modified in the current patch set as a sorted list. The purpose of
+   * this field is to allow matching changes that only touch files with certain file extensions.
+   */
+  public static final FieldDef<ChangeData, String> ONLY_EXTENSIONS =
+      exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS).build(ChangeField::getAllExtensionsAsList);
+
+  public static String getAllExtensionsAsList(ChangeData cd) {
+    return extensions(cd).distinct().sorted().collect(joining(","));
+  }
+
+  /**
+   * Returns a stream with all file extensions that are used by files in the given change. A file
+   * extension is defined as the portion of the filename following the final `.`. Files with no `.`
+   * in their name have no extension. For them an empty string is returned as part of the stream.
+   *
+   * <p>If the change contains multiple files with the same extension the extension is returned
+   * multiple times in the stream (once per file).
+   */
+  private static Stream<String> extensions(ChangeData cd) {
+    return cd.currentFilePaths().stream()
+        // Use case-insensitive file extensions even though other file fields are case-sensitive.
+        // If we want to find "all Java files", we want to match both .java and .JAVA, even if we
+        // normally care about case sensitivity. (Whether we should change the existing file/path
+        // predicates to be case insensitive is a separate question.)
+        .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
+  }
+
+  /** Footers from the commit message of the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> FOOTER =
+      exact(ChangeQueryBuilder.FIELD_FOOTER).buildRepeatable(ChangeField::getFooters);
+
+  public static Set<String> getFooters(ChangeData cd) {
+    return cd.commitFooters().stream()
+        .map(f -> f.toString().toLowerCase(Locale.US))
+        .collect(toSet());
+  }
+
+  /** Folders that are touched by the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY =
+      exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
+
+  public static Set<String> getDirectories(ChangeData cd) {
+    List<String> paths = cd.currentFilePaths();
+
+    Splitter s = Splitter.on('/').omitEmptyStrings();
+    Set<String> r = new HashSet<>();
+    for (String path : paths) {
+      StringBuilder directory = new StringBuilder();
+      directory.append("");
+      r.add(directory.toString());
+      String nextPart = null;
+      for (String part : s.split(path)) {
+        if (nextPart != null) {
+          r.add(nextPart);
+
+          if (directory.length() > 0) {
+            directory.append("/");
+          }
+          directory.append(nextPart);
+
+          String intermediateDir = directory.toString();
+          int i = intermediateDir.indexOf('/');
+          while (i >= 0) {
+            r.add(intermediateDir);
+            intermediateDir = intermediateDir.substring(i + 1);
+            i = intermediateDir.indexOf('/');
+          }
+        }
+        nextPart = part;
+      }
+    }
+    return r;
+  }
+
   /** Owner/creator of the change. */
   public static final FieldDef<ChangeData, Integer> OWNER =
       integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
@@ -372,7 +450,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
       exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
 
-  private static Set<String> getRevisions(ChangeData cd) throws OrmException {
+  private static Set<String> getRevisions(ChangeData cd) {
     Set<String> revisions = new HashSet<>();
     for (PatchSet ps : cd.patchSets()) {
       if (ps.getRevision() != null) {
@@ -391,7 +469,7 @@
   public static final FieldDef<ChangeData, Iterable<String>> LABEL =
       exact("label2").buildRepeatable(cd -> getLabels(cd, true));
 
-  private static Iterable<String> getLabels(ChangeData cd, boolean owners) throws OrmException {
+  private static Iterable<String> getLabels(ChangeData cd, boolean owners) {
     Set<String> allApprovals = new HashSet<>();
     Set<String> distinctApprovals = new HashSet<>();
     for (PatchSetApproval a : cd.currentApprovals()) {
@@ -408,20 +486,19 @@
     return allApprovals;
   }
 
-  public static Set<String> getAuthorParts(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getAuthorParts(ChangeData cd) {
     return SchemaUtil.getPersonParts(cd.getAuthor());
   }
 
-  public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getAuthorNameAndEmail(ChangeData cd) {
     return getNameAndEmail(cd.getAuthor());
   }
 
-  public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException {
+  public static Set<String> getCommitterParts(ChangeData cd) {
     return SchemaUtil.getPersonParts(cd.getCommitter());
   }
 
-  public static Set<String> getCommitterNameAndEmail(ChangeData cd)
-      throws OrmException, IOException {
+  public static Set<String> getCommitterNameAndEmail(ChangeData cd) {
     return getNameAndEmail(cd.getCommitter());
   }
 
@@ -743,8 +820,7 @@
 
   @VisibleForTesting
   static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
-    return values
-        .stream()
+    return values.stream()
         .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
         .collect(toList());
   }
@@ -758,7 +834,7 @@
     return storedSubmitRecords(cd.submitRecords(opts));
   }
 
-  public static List<String> formatSubmitRecordValues(ChangeData cd) throws OrmException {
+  public static List<String> formatSubmitRecordValues(ChangeData cd) {
     return formatSubmitRecordValues(
         cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner());
   }
@@ -846,7 +922,7 @@
                 return result;
               });
 
-  private static String getTopic(ChangeData cd) throws OrmException {
+  private static String getTopic(ChangeData cd) {
     Change c = cd.change();
     if (c == null) {
       return null;
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index f1120b2..348e0ce 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -37,7 +36,6 @@
 import com.google.inject.OutOfScopeException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -62,16 +60,6 @@
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
   }
 
-  @SuppressWarnings("deprecation")
-  public static com.google.common.util.concurrent.CheckedFuture<?, IOException> allAsList(
-      List<? extends ListenableFuture<?>> futures) {
-    // allAsList propagates the first seen exception, wrapped in
-    // ExecutionException, so we can reuse the same mapper as for a single
-    // future. Assume the actual contents of the exception are not useful to
-    // callers. All exceptions are already logged by IndexTask.
-    return Futures.makeChecked(Futures.allAsList(futures), IndexUtils.MAPPER);
-  }
-
   @Nullable private final ChangeIndexCollection indexes;
   @Nullable private final ChangeIndex index;
   private final ChangeData.Factory changeDataFactory;
@@ -134,9 +122,7 @@
    * @param id change to index.
    * @return future for the indexing task.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
-      Project.NameKey project, Change.Id id) {
+  public ListenableFuture<?> indexAsync(Project.NameKey project, Change.Id id) {
     return submit(new IndexTask(project, id));
   }
 
@@ -146,14 +132,12 @@
    * @param ids changes to index.
    * @return future for completing indexing of all changes.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
-      Project.NameKey project, Collection<Change.Id> ids) {
+  public ListenableFuture<?> indexAsync(Project.NameKey project, Collection<Change.Id> ids) {
     List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
       futures.add(indexAsync(project, id));
     }
-    return allAsList(futures);
+    return Futures.allAsList(futures);
   }
 
   /**
@@ -161,7 +145,7 @@
    *
    * @param cd change to index.
    */
-  public void index(ChangeData cd) throws IOException {
+  public void index(ChangeData cd) {
     indexImpl(cd);
 
     // Always double-check whether the change might be stale immediately after
@@ -185,7 +169,7 @@
     autoReindexIfStale(cd);
   }
 
-  private void indexImpl(ChangeData cd) throws IOException {
+  private void indexImpl(ChangeData cd) {
     logger.atFine().log("Replace change %d in index.", cd.getId().get());
     for (Index<?, ChangeData> i : getWriteIndexes()) {
       try (TraceTimer traceTimer =
@@ -211,7 +195,7 @@
    *
    * @param change change to index.
    */
-  public void index(Change change) throws IOException {
+  public void index(Change change) {
     index(changeDataFactory.create(change));
   }
 
@@ -221,7 +205,7 @@
    * @param project the project to which the change belongs.
    * @param changeId ID of the change to index.
    */
-  public void index(Project.NameKey project, Change.Id changeId) throws IOException {
+  public void index(Project.NameKey project, Change.Id changeId) {
     index(changeDataFactory.create(project, changeId));
   }
 
@@ -231,8 +215,7 @@
    * @param id change to delete.
    * @return future for the deleting task.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
+  public ListenableFuture<?> deleteAsync(Change.Id id) {
     return submit(new DeleteTask(id));
   }
 
@@ -241,7 +224,7 @@
    *
    * @param id change ID to delete.
    */
-  public void delete(Change.Id id) throws IOException {
+  public void delete(Change.Id id) {
     new DeleteTask(id).call();
   }
 
@@ -255,9 +238,7 @@
    * @param id ID of the change to index.
    * @return future for reindexing the change; returns true if the change was stale.
    */
-  @SuppressWarnings("deprecation")
-  public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
-      Project.NameKey project, Change.Id id) {
+  public ListenableFuture<Boolean> reindexIfStale(Project.NameKey project, Change.Id id) {
     return submit(new ReindexIfStaleTask(project, id), batchExecutor);
   }
 
@@ -277,17 +258,13 @@
     return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index);
   }
 
-  @SuppressWarnings("deprecation")
-  private <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
-      Callable<T> task) {
+  private <T> ListenableFuture<T> submit(Callable<T> task) {
     return submit(task, executor);
   }
 
-  @SuppressWarnings("deprecation")
-  private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
+  private static <T> ListenableFuture<T> submit(
       Callable<T> task, ListeningExecutorService executor) {
-    return Futures.makeChecked(
-        Futures.nonCancellationPropagating(executor.submit(task)), IndexUtils.MAPPER);
+    return Futures.nonCancellationPropagating(executor.submit(task));
   }
 
   private abstract class AbstractIndexTask<T> implements Callable<T> {
@@ -351,7 +328,7 @@
     }
 
     @Override
-    public Void call() throws IOException {
+    public Void call() {
       logger.atFine().log("Delete change %d from index.", id.get());
       // Don't bother setting a RequestContext to provide the DB.
       // Implementations should not need to access the DB in order to delete a
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 9016fd1..cde6a64 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -101,7 +101,18 @@
   // Bump Lucene version requires reindexing
   @Deprecated static final Schema<ChangeData> V50 = schema(V49);
 
-  static final Schema<ChangeData> V51 = schema(V50, ChangeField.TOTAL_COMMENT_COUNT);
+  @Deprecated static final Schema<ChangeData> V51 = schema(V50, ChangeField.TOTAL_COMMENT_COUNT);
+
+  @Deprecated static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
+
+  @Deprecated static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
+
+  @Deprecated static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
+
+  @Deprecated static final Schema<ChangeData> V55 = schema(V54, ChangeField.DIRECTORY);
+
+  // The computation of the 'extension' field is changed, hence reindexing is required.
+  static final Schema<ChangeData> V56 = schema(V55);
 
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
index f6cee6d..9be93f7 100644
--- a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
-import java.io.IOException;
 
 public class DummyChangeIndex implements ChangeIndex {
   @Override
@@ -32,13 +31,13 @@
   public void close() {}
 
   @Override
-  public void replace(ChangeData cd) throws IOException {}
+  public void replace(ChangeData cd) {}
 
   @Override
-  public void delete(Change.Id id) throws IOException {}
+  public void delete(Change.Id id) {}
 
   @Override
-  public void deleteAll() throws IOException {}
+  public void deleteAll() {}
 
   @Override
   public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts) {
@@ -46,7 +45,7 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {}
+  public void markReady(boolean ready) {}
 
   public int getMaxLimit() {
     return Integer.MAX_VALUE;
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 8088837..ed09eed 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -81,7 +80,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
+  public ResultSet<ChangeData> read() {
     final DataSource<ChangeData> currSource = source;
     final ResultSet<ChangeData> rs = currSource.read();
 
@@ -114,7 +113,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (source != null && fromSource.get(cd) == source) {
       return true;
     }
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index 669bb1e..4062988 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -91,12 +90,8 @@
     if (allUsersName.get().equals(event.getProjectName())) {
       Account.Id accountId = Account.Id.fromRef(event.getRefName());
       if (accountId != null && !event.getRefName().startsWith(RefNames.REFS_STARRED_CHANGES)) {
-        try {
-          accountCache.evict(accountId);
-          indexer.get().index(accountId);
-        } catch (IOException e) {
-          logger.atSevere().withCause(e).log("Reindex account %s failed.", accountId);
-        }
+        accountCache.evict(accountId);
+        indexer.get().index(accountId);
       }
     }
 
@@ -152,7 +147,7 @@
     }
 
     @Override
-    protected List<Change> impl(RequestContext ctx) throws OrmException {
+    protected List<Change> impl(RequestContext ctx) {
       String ref = event.getRefName();
       Project.NameKey project = new Project.NameKey(event.getProjectName());
       if (ref.equals(RefNames.REFS_CONFIG)) {
@@ -179,7 +174,7 @@
     }
 
     @Override
-    protected Void impl(RequestContext ctx) throws OrmException, IOException {
+    protected Void impl(RequestContext ctx) throws IOException {
       // Reload change, as some time may have passed since GetChanges.
       try {
         Change c =
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index f573a84..338cf3d 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -28,12 +28,12 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
+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.server.UsedAt;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
@@ -68,7 +68,7 @@
     this.indexConfig = indexConfig;
   }
 
-  public boolean isStale(Change.Id id) throws IOException {
+  public boolean isStale(Change.Id id) {
     ChangeIndex i = indexes.getSearchIndex();
     if (i == null) {
       return false; // No index; caller couldn't do anything if it is stale.
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java
index 503fd6b..5d9232e 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexer.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index.group;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.io.IOException;
 
 public interface GroupIndexer {
 
@@ -24,7 +23,7 @@
    *
    * @param uuid group UUID to index.
    */
-  void index(AccountGroup.UUID uuid) throws IOException;
+  void index(AccountGroup.UUID uuid);
 
   /**
    * Synchronously reindex a group if it is stale.
@@ -32,5 +31,5 @@
    * @param uuid group UUID to index.
    * @return whether the group was reindexed
    */
-  boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException;
+  boolean reindexIfStale(AccountGroup.UUID uuid);
 }
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index a9124e1..5982de7 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public void index(AccountGroup.UUID uuid) throws IOException {
+  public void index(AccountGroup.UUID uuid) {
     // Evict the cache to get an up-to-date value for sure.
     groupCache.evict(uuid);
     Optional<InternalGroup> internalGroup = groupCache.get(uuid);
@@ -104,10 +105,14 @@
   }
 
   @Override
-  public boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException {
-    if (stalenessChecker.isStale(uuid)) {
-      index(uuid);
-      return true;
+  public boolean reindexIfStale(AccountGroup.UUID uuid) {
+    try {
+      if (stalenessChecker.isStale(uuid)) {
+        index(uuid);
+        return true;
+      }
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index 1a74eca..6d6f78d 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -71,7 +70,7 @@
   }
 
   @Override
-  public void index(Project.NameKey nameKey) throws IOException {
+  public void index(Project.NameKey nameKey) {
     ProjectState projectState = projectCache.get(nameKey);
     if (projectState != null) {
       logger.atFine().log("Replace project %s in index", nameKey.get());
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index 25cc5fa..dc5ebc6 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.Optional;
 
 public class StalenessChecker {
@@ -48,7 +47,7 @@
     this.indexConfig = indexConfig;
   }
 
-  public boolean isStale(Project.NameKey project) throws IOException {
+  public boolean isStale(Project.NameKey project) {
     ProjectData projectData = projectCache.get(project).toProjectData();
     ProjectIndex i = indexes.getSearchIndex();
     if (i == null) {
@@ -66,9 +65,7 @@
 
     SetMultimap<Project.NameKey, RefState> currentRefStates =
         MultimapBuilder.hashKeys().hashSetValues().build();
-    projectData
-        .tree()
-        .stream()
+    projectData.tree().stream()
         .filter(p -> p.getProject().getConfigRefState() != null)
         .forEach(
             p ->
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java
index 62f2bbc..c27dbbb 100644
--- a/java/com/google/gerrit/server/logging/CallerFinder.java
+++ b/java/com/google/gerrit/server/logging/CallerFinder.java
@@ -170,8 +170,7 @@
   public LazyArg<String> findCaller() {
     return lazy(
         () ->
-            targets()
-                .stream()
+            targets().stream()
                 .map(t -> findCallerOf(t, skip() + 1))
                 .filter(Optional::isPresent)
                 .findFirst()
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 76968d5..5c0406d 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -134,10 +134,7 @@
     }
 
     Optional<String> existingTraceId =
-        LoggingContext.getInstance()
-            .getTagsAsMap()
-            .get(RequestId.Type.TRACE_ID.name())
-            .stream()
+        LoggingContext.getInstance().getTagsAsMap().get(RequestId.Type.TRACE_ID.name()).stream()
             .findAny();
     if (existingTraceId.isPresent()) {
       // request tracing was already started, no need to generate a new trace ID
@@ -209,6 +206,23 @@
     return new TraceTimer(format, arg1, arg2, arg3);
   }
 
+  /**
+   * Opens a new timer that logs the time for an operation if request tracing is enabled.
+   *
+   * <p>If request tracing is not enabled this is a no-op.
+   *
+   * @param format the message format string
+   * @param arg1 first argument for the message
+   * @param arg2 second argument for the message
+   * @param arg3 third argument for the message
+   * @param arg4 fourth argument for the message
+   * @return the trace timer
+   */
+  public static TraceTimer newTimer(
+      String format, Object arg1, Object arg2, Object arg3, Object arg4) {
+    return new TraceTimer(format, arg1, arg2, arg3, arg4);
+  }
+
   public static class TraceTimer implements AutoCloseable {
     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
@@ -232,6 +246,16 @@
       this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, arg3, elapsedMs));
     }
 
+    private TraceTimer(
+        String format,
+        @Nullable Object arg1,
+        @Nullable Object arg2,
+        @Nullable Object arg3,
+        @Nullable Object arg4) {
+      this(
+          elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, arg3, arg4, elapsedMs));
+    }
+
     private TraceTimer(Consumer<Long> logFn) {
       this.logFn = logFn;
       this.stopwatch = Stopwatch.createStarted();
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index 185e7f0..4afcc7b 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -18,17 +18,17 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.gerrit.common.FooterConstants;
-import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 
@@ -36,7 +36,7 @@
 
   public static MailRecipients getRecipientsFromFooters(
       AccountResolver accountResolver, List<FooterLine> footerLines)
-      throws OrmException, IOException {
+      throws IOException, ConfigInvalidException {
     MailRecipients recipients = new MailRecipients();
     for (FooterLine footerLine : footerLines) {
       try {
@@ -45,7 +45,7 @@
         } else if (footerLine.matches(FooterKey.CC)) {
           recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
         }
-      } catch (NoSuchAccountException e) {
+      } catch (UnprocessableEntityException e) {
         continue;
       }
     }
@@ -59,13 +59,10 @@
     return recipients;
   }
 
+  @SuppressWarnings("deprecation")
   private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
-      throws OrmException, NoSuchAccountException, IOException {
-    Account a = accountResolver.findByNameOrEmail(nameOrEmail);
-    if (a == null) {
-      throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
-    }
-    return a.getId();
+      throws UnprocessableEntityException, IOException, ConfigInvalidException {
+    return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().getAccount().getId();
   }
 
   private static boolean isReviewer(FooterLine candidateFooterLine) {
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index a1d745e..b1acab9 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -17,11 +17,11 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -65,7 +65,6 @@
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -98,7 +97,7 @@
   private final CommentAdded commentAdded;
   private final ApprovalsUtil approvalsUtil;
   private final AccountCache accountCache;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
   public MailProcessor(
@@ -116,7 +115,7 @@
       ApprovalsUtil approvalsUtil,
       CommentAdded commentAdded,
       AccountCache accountCache,
-      UrlFormatter urlFormatter) {
+      DynamicItem<UrlFormatter> urlFormatter) {
     this.emails = emails;
     this.emailRejectionSender = emailRejectionSender;
     this.retryHelper = retryHelper;
@@ -148,7 +147,7 @@
   }
 
   private void processImpl(BatchUpdate.Factory buf, MailMessage message)
-      throws OrmException, UpdateException, RestApiException, IOException {
+      throws UpdateException, RestApiException, IOException {
     for (Extension<MailFilter> filter : mailFilters) {
       if (!filter.getProvider().get().shouldProcessMessage(message)) {
         logger.atWarning().log(
@@ -209,7 +208,7 @@
 
   private void persistComments(
       BatchUpdate.Factory buf, MailMessage message, MailMetadata metadata, Account.Id sender)
-      throws OrmException, UpdateException, RestApiException {
+      throws UpdateException, RestApiException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(sender)) {
       List<ChangeData> changeDataList =
           queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
@@ -232,8 +231,7 @@
       // comments from the outbound email.
       // TODO(hiesel) Also filter by original comment author.
       Collection<Comment> comments =
-          cd.publishedComments()
-              .stream()
+          cd.publishedComments().stream()
               .filter(c -> (c.writtenOn.getTime() / 1000) == (metadata.timestamp.getTime() / 1000))
               .sorted(CommentsUtil.COMMENT_ORDER)
               .collect(toList());
@@ -242,7 +240,10 @@
       // If URL is not defined, we won't be able to parse line comments. We still attempt to get the
       // other ones.
       String changeUrl =
-          urlFormatter.getChangeViewUrl(cd.project(), cd.getId()).orElse("http://gerrit.invalid/");
+          urlFormatter
+              .get()
+              .getChangeViewUrl(cd.project(), cd.getId())
+              .orElse("http://gerrit.invalid/");
 
       List<MailComment> parsedComments;
       if (useHtmlParser(message)) {
@@ -282,11 +283,11 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
+        throws UnprocessableEntityException, PatchListNotAvailableException {
       patchSet = psUtil.get(ctx.getNotes(), psId);
       notes = ctx.getNotes();
       if (patchSet == null) {
-        throw new OrmException("patch set not found: " + psId);
+        throw new StorageException("patch set not found: " + psId);
       }
 
       changeMessage = generateChangeMessage(ctx);
@@ -315,8 +316,7 @@
       // Send email notifications
       outgoingMailFactory
           .create(
-              NotifyHandling.ALL,
-              ArrayListMultimap.create(),
+              ctx.getNotify(notes.getChangeId()),
               notes,
               patchSet,
               ctx.getUser().asIdentifiedUser(),
@@ -358,7 +358,7 @@
     }
 
     private PatchSet targetPatchSetForComment(
-        ChangeContext ctx, MailComment mailComment, PatchSet current) throws OrmException {
+        ChangeContext ctx, MailComment mailComment, PatchSet current) {
       if (mailComment.getInReplyTo() != null) {
         return psUtil.get(
             ctx.getNotes(),
@@ -369,7 +369,7 @@
 
     private Comment persistentCommentFromMailComment(
         ChangeContext ctx, MailComment mailComment, PatchSet patchSetForComment)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
+        throws UnprocessableEntityException, PatchListNotAvailableException {
       String fileName;
       // The patch set that this comment is based on is different if this
       // comment was sent in reply to a comment on a previous patch set.
@@ -412,10 +412,9 @@
     return "(" + numComments + (numComments > 1 ? " comments)" : " comment)");
   }
 
-  private Set<String> existingMessageIds(ChangeData cd) throws OrmException {
+  private Set<String> existingMessageIds(ChangeData cd) {
     Set<String> existingMessageIds = new HashSet<>();
-    cd.messages()
-        .stream()
+    cd.messages().stream()
         .forEach(
             m -> {
               String messageId = CommentsUtil.extractMessageId(m.getTag());
@@ -423,8 +422,7 @@
                 existingMessageIds.add(messageId);
               }
             });
-    cd.publishedComments()
-        .stream()
+    cd.publishedComments().stream()
         .forEach(
             c -> {
               String messageId = CommentsUtil.extractMessageId(c.tag);
diff --git a/java/com/google/gerrit/server/mail/send/AbandonedSender.java b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
index 05dd542..1017965 100644
--- a/java/com/google/gerrit/server/mail/send/AbandonedSender.java
+++ b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   public AbandonedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "abandon", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 433bd9b..da866f4 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.mail.Address;
diff --git a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
index cb70106..22abd9c 100644
--- a/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -29,8 +28,7 @@
 
   @Inject
   public AddReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 09d3e51..4905823 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -18,7 +18,8 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -43,7 +44,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.template.soy.data.SoyListData;
 import com.google.template.soy.data.SoyMapData;
 import java.io.IOException;
@@ -84,7 +84,7 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) throws OrmException {
+  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) {
     super(ea, mc, cd.change().getDest());
     changeData = cd;
     change = cd.change();
@@ -150,7 +150,7 @@
     if (patchSet == null) {
       try {
         patchSet = changeData.currentPatchSet();
-      } catch (OrmException err) {
+      } catch (StorageException err) {
         patchSet = null;
       }
     }
@@ -160,7 +160,7 @@
       if (patchSetInfo == null) {
         try {
           patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.getId());
-        } catch (PatchSetInfoNotAvailableException | OrmException err) {
+        } catch (PatchSetInfoNotAvailableException | StorageException err) {
           patchSetInfo = null;
         }
       }
@@ -169,7 +169,7 @@
 
     try {
       stars = changeData.stars();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new EmailException("Failed to load stars for change " + change.getChangeId(), e);
     }
 
@@ -183,14 +183,14 @@
     setChangeUrlHeader();
     setCommitIdHeader();
 
-    if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+    if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
       try {
         addByEmail(
             RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC));
         addByEmail(
             RecipientType.CC,
             changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
       }
     }
@@ -219,7 +219,10 @@
   /** Get a link to the change; null if the server doesn't know its own address. */
   @Nullable
   public String getChangeUrl() {
-    return args.urlFormatter.getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
+    return args.urlFormatter
+        .get()
+        .getChangeViewUrl(change.getProject(), change.getId())
+        .orElse(null);
   }
 
   public String getChangeMessageThreadId() {
@@ -283,6 +286,21 @@
     }
   }
 
+  /** Get the patch list corresponding to patch set patchSetId of this change. */
+  protected PatchList getPatchList(int patchSetId) throws PatchListNotAvailableException {
+    PatchSet ps;
+    if (patchSetId == patchSet.getPatchSetId()) {
+      ps = patchSet;
+    } else {
+      try {
+        ps = args.patchSetUtil.get(changeData.notes(), new PatchSet.Id(change.getId(), patchSetId));
+      } catch (StorageException e) {
+        throw new PatchListNotAvailableException("Failed to get patchSet");
+      }
+    }
+    return args.patchListCache.get(change, ps);
+  }
+
   /** Get the patch list corresponding to this patch set. */
   protected PatchList getPatchList() throws PatchListNotAvailableException {
     if (patchSet != null) {
@@ -305,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;
     }
 
@@ -325,9 +343,8 @@
   }
 
   @Override
-  protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException {
-    if (!NotifyHandling.ALL.equals(notify)) {
+  protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
+    if (!NotifyHandling.ALL.equals(notify.handling())) {
       return new Watchers();
     }
 
@@ -337,7 +354,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;
     }
 
@@ -345,14 +363,15 @@
       for (Account.Id id : changeData.reviewers().all()) {
         add(RecipientType.CC, id);
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change");
     }
   }
 
   /** Users who have non-zero approval codes on the change. */
   protected void ccExistingReviewers() {
-    if (!NotifyHandling.ALL.equals(notify) && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) {
+    if (!NotifyHandling.ALL.equals(notify.handling())
+        && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
       return;
     }
 
@@ -360,7 +379,7 @@
       for (Account.Id id : changeData.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
         add(RecipientType.CC, id);
       }
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change");
     }
   }
@@ -389,7 +408,7 @@
   protected Set<Account.Id> getAuthors() {
     Set<Account.Id> authors = new HashSet<>();
 
-    switch (notify) {
+    switch (notify.handling()) {
       case NONE:
         break;
       case ALL:
@@ -486,7 +505,7 @@
       for (Account.Id who : changeData.reviewers().byState(state)) {
         reviewers.add(getNameEmailFor(who));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log("Cannot get change reviewers");
     }
     return reviewers;
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index c8aa259..01d8a17 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -20,8 +20,9 @@
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.FilenameComparator;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.NoSuchEntityException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.mail.MailProcessingUtil;
@@ -41,7 +42,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -119,8 +119,7 @@
       CommentsUtil commentsUtil,
       @GerritServerConfig Config cfg,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
+      @Assisted Change.Id id) {
     super(ea, "comment", newChangeData(ea, project, id));
     this.commentsUtil = commentsUtil;
     this.incomingEmailEnabled =
@@ -129,7 +128,7 @@
     this.replyToAddress = cfg.getString("sendemail", null, "replyToAddress");
   }
 
-  public void setComments(List<Comment> comments) throws OrmException {
+  public void setComments(List<Comment> comments) {
     inlineComments = comments;
 
     Set<String> paths = new HashSet<>();
@@ -153,10 +152,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());
     }
@@ -198,17 +197,6 @@
    */
   private List<CommentSender.FileCommentGroup> getGroupedInlineComments(Repository repo) {
     List<CommentSender.FileCommentGroup> groups = new ArrayList<>();
-    // Get the patch list:
-    PatchList patchList = null;
-    if (repo != null) {
-      try {
-        patchList = getPatchList();
-      } catch (PatchListObjectTooLargeException e) {
-        logger.atWarning().log("Failed to get patch list: %s", e.getMessage());
-      } catch (PatchListNotAvailableException e) {
-        logger.atSevere().withCause(e).log("Failed to get patch list");
-      }
-    }
 
     // Loop over the comments and collect them into groups based on the file
     // location of the comment.
@@ -221,6 +209,16 @@
         currentGroup = new FileCommentGroup();
         currentGroup.filename = c.key.filename;
         currentGroup.patchSetId = c.key.patchSetId;
+        // Get the patch list:
+        PatchList patchList = null;
+        try {
+          patchList = getPatchList(c.key.patchSetId);
+        } catch (PatchListObjectTooLargeException e) {
+          logger.atWarning().log("Failed to get patch list: %s", e.getMessage());
+        } catch (PatchListNotAvailableException e) {
+          logger.atSevere().withCause(e).log("Failed to get patch list");
+        }
+
         groups.add(currentGroup);
         if (patchList != null) {
           try {
@@ -317,7 +315,7 @@
     Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, child.key.patchSetId);
     try {
       return commentsUtil.getPublished(changeData.notes(), key);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().log("Could not find the parent of this comment: %s", child);
       return Optional.empty();
     }
@@ -459,8 +457,7 @@
   }
 
   private List<Map<String, Object>> commentBlocksToSoyData(List<CommentFormatter.Block> blocks) {
-    return blocks
-        .stream()
+    return blocks.stream()
         .map(
             b -> {
               Map<String, Object> map = new HashMap<>();
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index fc9c14a..9895e07 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.stream.StreamSupport;
@@ -44,8 +44,7 @@
       EmailArguments ea,
       PermissionBackend permissionBackend,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
+      @Assisted Change.Id id) {
     super(ea, newChangeData(ea, project, id));
     this.permissionBackend = permissionBackend;
   }
@@ -66,7 +65,7 @@
       add(RecipientType.TO, matching.to);
       add(RecipientType.CC, matching.cc);
       add(RecipientType.BCC, matching.bcc);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index 576d506..f941acc 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -42,8 +41,7 @@
 
   @Inject
   public DeleteReviewerSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "deleteReviewer", newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
index 0c81293..195d53d 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   protected DeleteVoteSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "deleteVote", newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index a3674cd..fe2f74b 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -14,13 +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.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.UsedAt;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -56,6 +58,7 @@
   final GroupBackend groupBackend;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
+  final PatchSetUtil patchSetUtil;
   final ApprovalsUtil approvalsUtil;
   final FromAddressGenerator fromAddressGenerator;
   final EmailSender emailSender;
@@ -65,7 +68,7 @@
   final AnonymousUser anonymousUser;
   final String anonymousCowardName;
   final PersonIdent gerritPersonIdent;
-  final UrlFormatter urlFormatter;
+  final DynamicItem<UrlFormatter> urlFormatter;
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
   final SitePaths site;
@@ -88,6 +91,7 @@
       GroupBackend groupBackend,
       AccountCache accountCache,
       PatchListCache patchListCache,
+      PatchSetUtil patchSetUtil,
       ApprovalsUtil approvalsUtil,
       FromAddressGenerator fromAddressGenerator,
       EmailSender emailSender,
@@ -97,7 +101,7 @@
       AnonymousUser anonymousUser,
       @AnonymousCowardName String anonymousCowardName,
       GerritPersonIdentProvider gerritPersonIdentProvider,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder queryBuilder,
       ChangeData.Factory changeDataFactory,
@@ -116,6 +120,7 @@
     this.groupBackend = groupBackend;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
+    this.patchSetUtil = patchSetUtil;
     this.approvalsUtil = approvalsUtil;
     this.fromAddressGenerator = fromAddressGenerator;
     this.emailSender = emailSender;
diff --git a/java/com/google/gerrit/server/mail/send/EmailSender.java b/java/com/google/gerrit/server/mail/send/EmailSender.java
index ce4964d..9b3a1f7 100644
--- a/java/com/google/gerrit/server/mail/send/EmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/EmailSender.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import java.util.Collection;
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 99edc04..b5d384d 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -16,7 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailHeader;
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index b524c83..cbc4117 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -19,13 +19,13 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -38,8 +38,8 @@
   private final LabelTypes labelTypes;
 
   @Inject
-  public MergedSender(EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+  public MergedSender(
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "merged", newChangeData(ea, project, id));
     labelTypes = changeData.getLabelTypes();
   }
@@ -82,7 +82,7 @@
       }
 
       return format("Approvals", pos) + format("Objections", neg);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Don't list the approvals
     }
     return "";
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index f94f1ca..7a4fede 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -33,7 +32,7 @@
   private final Set<Account.Id> extraCC = new HashSet<>();
   private final Set<Address> extraCCByEmail = new HashSet<>();
 
-  protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException {
+  protected NewChangeSender(EmailArguments ea, ChangeData cd) {
     super(ea, "newchange", cd);
   }
 
@@ -59,7 +58,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/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 032bcbf..64047bf 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -17,7 +17,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailHeader;
@@ -25,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
-import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -68,7 +68,7 @@
       add(RecipientType.TO, matching.to);
       add(RecipientType.CC, matching.cc);
       add(RecipientType.BCC, matching.bcc);
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
@@ -77,8 +77,7 @@
   }
 
   /** Returns all watchers that are relevant */
-  protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException;
+  protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig);
 
   /** Add users or email addresses to the TO, CC, or BCC list. */
   protected void add(RecipientType type, Watchers.List list) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 043eee9..db97f06 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.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
@@ -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);
@@ -299,7 +292,7 @@
   }
 
   public String getGerritUrl() {
-    return args.urlFormatter.getWebUrl().orElse(null);
+    return args.urlFormatter.get().getWebUrl().orElse(null);
   }
 
   /** Set a header in the outgoing message. */
@@ -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/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 9a86fdb..fd006c2 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.SingleGroupUser;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -62,8 +61,7 @@
   }
 
   /** Returns all watchers that are relevant */
-  public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
-      throws OrmException {
+  public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
     Watchers matching = new Watchers();
     Set<Account.Id> projectWatchers = new HashSet<>();
 
@@ -148,7 +146,7 @@
     }
   }
 
-  private void add(Watchers matching, NotifyConfig nc) throws OrmException, QueryParseException {
+  private void add(Watchers matching, NotifyConfig nc) throws QueryParseException {
     for (GroupReference ref : nc.getGroups()) {
       CurrentUser user = new SingleGroupUser(ref.getUUID());
       if (filterMatch(user, nc.getFilter())) {
@@ -202,8 +200,7 @@
       Account.Id accountId,
       ProjectWatchKey key,
       Set<NotifyType> watchedTypes,
-      NotifyType type)
-      throws OrmException {
+      NotifyType type) {
     IdentifiedUser user = args.identifiedUserFactory.create(accountId);
 
     try {
@@ -221,8 +218,7 @@
     return false;
   }
 
-  private boolean filterMatch(CurrentUser user, String filter)
-      throws OrmException, QueryParseException {
+  private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
     ChangeQueryBuilder qb;
     Predicate<ChangeData> p = null;
 
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index 3bca00c..436736b 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -16,7 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 2398b82..30bcdeb 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -41,8 +40,7 @@
 
   @Inject
   public ReplacePatchSetSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "newpatchset", newChangeData(ea, project, id));
   }
 
@@ -63,7 +61,8 @@
       //
       reviewers.remove(fromId);
     }
-    if (notify == NotifyHandling.ALL || notify == NotifyHandling.OWNER_REVIEWERS) {
+    if (notify.handling() == NotifyHandling.ALL
+        || notify.handling() == NotifyHandling.OWNER_REVIEWERS) {
       add(RecipientType.TO, reviewers);
       add(RecipientType.CC, extraCC);
     }
diff --git a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
index 61e9d1d..960c3a8 100644
--- a/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends ChangeEmail {
@@ -27,7 +26,7 @@
     T create(Project.NameKey project, Change.Id id);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) throws OrmException {
+  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) {
     super(ea, mc, cd);
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/RestoredSender.java b/java/com/google/gerrit/server/mail/send/RestoredSender.java
index d7f8eb5..0d998aa 100644
--- a/java/com/google/gerrit/server/mail/send/RestoredSender.java
+++ b/java/com/google/gerrit/server/mail/send/RestoredSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -31,8 +30,7 @@
 
   @Inject
   public RestoredSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "restore", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/RevertedSender.java b/java/com/google/gerrit/server/mail/send/RevertedSender.java
index 21703a3..48b5d99 100644
--- a/java/com/google/gerrit/server/mail/send/RevertedSender.java
+++ b/java/com/google/gerrit/server/mail/send/RevertedSender.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -30,8 +29,7 @@
 
   @Inject
   public RevertedSender(
-      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
-      throws OrmException {
+      EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) {
     super(ea, "revert", ChangeEmail.newChangeData(ea, project, id));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
index 9708b1b..a120769 100644
--- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
+++ b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.mail.send;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -35,8 +34,7 @@
       EmailArguments ea,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id,
-      @Assisted Account.Id assignee)
-      throws OrmException {
+      @Assisted Account.Id assignee) {
     super(ea, "setassignee", newChangeData(ea, project, id));
     this.assignee = assignee;
   }
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 8615c04..3f103fc 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -20,7 +20,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -170,7 +170,7 @@
       throw new EmailException("Sending email is disabled");
     }
 
-    StringBuffer rejected = new StringBuffer();
+    StringBuilder rejected = new StringBuilder();
     try {
       final SMTPClient client = open();
       try {
@@ -207,10 +207,11 @@
              */
             throw new EmailException(
                 rejected
-                    + "Server "
-                    + smtpHost
-                    + " rejected DATA command: "
-                    + client.getReplyString());
+                    .append("Server ")
+                    .append(smtpHost)
+                    .append(" rejected DATA command: ")
+                    .append(client.getReplyString())
+                    .toString());
           }
 
           render(messageDataWriter, callerHeaders, textBody, htmlBody);
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 7deac54..1c6057d 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -19,6 +19,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -26,7 +28,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 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;
@@ -41,20 +42,20 @@
 public abstract class AbstractChangeNotes<T> {
   @VisibleForTesting
   @Singleton
+  @UsedAt(UsedAt.Project.PLUGIN_CHECKS)
   public static class Args {
     // TODO(dborowitz): Some less smelly way of disabling NoteDb in tests.
     public final AtomicBoolean failOnLoadForTest;
-
-    final GitRepositoryManager repoManager;
-    final AllUsersName allUsers;
-    final ChangeNoteJson changeNoteJson;
-    final LegacyChangeNoteRead legacyChangeNoteRead;
-    final NoteDbMetrics metrics;
+    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.
 
     // ChangeNoteCache -> Args
-    final Provider<ChangeNotesCache> cache;
+    public final Provider<ChangeNotesCache> cache;
 
     @Inject
     Args(
@@ -116,7 +117,7 @@
   private ObjectId revision;
   private boolean loaded;
 
-  AbstractChangeNotes(Args args, Change.Id changeId) {
+  protected AbstractChangeNotes(Args args, Change.Id changeId) {
     this.args = requireNonNull(args);
     this.changeId = requireNonNull(changeId);
   }
@@ -130,13 +131,13 @@
     return revision;
   }
 
-  public T load() throws OrmException {
+  public T load() {
     if (loaded) {
       return self();
     }
 
     if (args.failOnLoadForTest.get()) {
-      throw new OrmException("Reading from NoteDb is disabled");
+      throw new StorageException("Reading from NoteDb is disabled");
     }
     try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
         Repository repo = args.repoManager.openRepository(getProjectName());
@@ -147,7 +148,7 @@
       onLoad(handle);
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
     return self();
   }
@@ -175,12 +176,12 @@
     return new LoadHandle(repo, id);
   }
 
-  public T reload() throws OrmException {
+  public T reload() {
     loaded = false;
     return load();
   }
 
-  public ObjectId loadRevision() throws OrmException {
+  public ObjectId loadRevision() {
     if (loaded) {
       return getRevision();
     }
@@ -188,7 +189,7 @@
       Ref ref = repo.getRefDatabase().exactRef(getRefName());
       return ref != null ? ref.getObjectId() : null;
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index daac83f..b9ce863 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Date;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -186,6 +185,14 @@
   protected abstract String getRefName();
 
   /**
+   * Whether to allow bypassing the check that an update does not exceed the max update count on an
+   * object.
+   */
+  protected boolean bypassMaxUpdates() {
+    return false;
+  }
+
+  /**
    * Apply this update to the given inserter.
    *
    * @param rw walk for reading back any objects needed for the update.
@@ -194,11 +201,9 @@
    * @return commit ID produced by inserting this update's commit, or null if this update is a no-op
    *     and should be skipped. The zero ID is a valid return value, and indicates the ref should be
    *     deleted.
-   * @throws OrmException if a Gerrit-level error occurred.
    * @throws IOException if a lower-level error occurred.
    */
-  final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+  final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
     if (isEmpty()) {
       return null;
     }
@@ -246,11 +251,10 @@
    *     indicates to the caller that it should be copied from the parent commit. To indicate that
    *     this update is a no-op (but this could not be determined by {@link #isEmpty()}), return the
    *     sentinel {@link #NO_OP_UPDATE}.
-   * @throws OrmException if a Gerrit-level error occurred.
    * @throws IOException if a lower-level error occurred.
    */
   protected abstract CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException;
+      throws IOException;
 
   protected static final CommitBuilder NO_OP_UPDATE = new CommitBuilder();
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 5a3ebd6..38b7b12 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,6 +19,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -28,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -132,7 +132,7 @@
 
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
     Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
@@ -184,7 +184,7 @@
   }
 
   private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     // The old DraftCommentNotes already parsed the revision notes. We can reuse them as long as
     // the ref hasn't advanced.
     ChangeNotes changeNotes = getNotes();
@@ -217,13 +217,13 @@
 
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     CommitBuilder cb = new CommitBuilder();
     cb.setMessage("Update draft comments");
     try {
       return storeCommentsInNotes(rw, ins, curr, cb);
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index af85661..a4e8513 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -36,6 +36,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -55,7 +56,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -103,17 +103,16 @@
       this.projectCache = projectCache;
     }
 
-    public ChangeNotes createChecked(Change c) throws OrmException {
+    public ChangeNotes createChecked(Change c) {
       return createChecked(c.getProject(), c.getId());
     }
 
-    public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId)
-        throws OrmException {
+    public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) {
       Change change = newChange(project, changeId);
       return new ChangeNotes(args, change, true, null).load();
     }
 
-    public ChangeNotes createChecked(Change.Id changeId) throws OrmException {
+    public ChangeNotes createChecked(Change.Id changeId) {
       InternalChangeQuery query = queryProvider.get().noFields();
       List<ChangeData> changes = query.byLegacyChangeId(changeId);
       if (changes.isEmpty()) {
@@ -131,7 +130,7 @@
           null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null);
     }
 
-    public ChangeNotes create(Project.NameKey project, Change.Id changeId) throws OrmException {
+    public ChangeNotes create(Project.NameKey project, Change.Id changeId) {
       checkArgument(project != null, "project is required");
       return new ChangeNotes(args, newChange(project, changeId), true, null).load();
     }
@@ -147,16 +146,15 @@
       return new ChangeNotes(args, change, true, null);
     }
 
-    public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist)
-        throws OrmException {
+    public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist) {
       return new ChangeNotes(args, change, shouldExist, null).load();
     }
 
-    public ChangeNotes create(Change change, RefCache refs) throws OrmException {
+    public ChangeNotes create(Change change, RefCache refs) {
       return new ChangeNotes(args, change, true, refs).load();
     }
 
-    public List<ChangeNotes> create(Collection<Change.Id> changeIds) throws OrmException {
+    public List<ChangeNotes> create(Collection<Change.Id> changeIds) {
       List<ChangeNotes> notes = new ArrayList<>();
       for (Change.Id changeId : changeIds) {
         try {
@@ -169,8 +167,9 @@
     }
 
     public List<ChangeNotes> create(
-        Project.NameKey project, Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate)
-        throws OrmException {
+        Project.NameKey project,
+        Collection<Change.Id> changeIds,
+        Predicate<ChangeNotes> predicate) {
       List<ChangeNotes> notes = new ArrayList<>();
       for (Change.Id cid : changeIds) {
         try {
@@ -229,7 +228,7 @@
       ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null);
       try {
         n.load();
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return ChangeNotesResult.error(n.getChangeId(), e);
       }
       return ChangeNotesResult.notes(n);
@@ -238,7 +237,7 @@
     /** Result of {@link #scan(Repository,Project.NameKey)}. */
     @AutoValue
     public abstract static class ChangeNotesResult {
-      static ChangeNotesResult error(Change.Id id, OrmException e) {
+      static ChangeNotesResult error(Change.Id id, StorageException e) {
         return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(id, Optional.of(e), null);
       }
 
@@ -251,7 +250,7 @@
       public abstract Change.Id id();
 
       /** Error encountered while loading this change, if any. */
-      public abstract Optional<OrmException> error();
+      public abstract Optional<StorageException> error();
 
       /**
        * Notes loaded for this change.
@@ -419,13 +418,16 @@
     return commentKeys;
   }
 
-  public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author)
-      throws OrmException {
+  public int getUpdateCount() {
+    return state.updateCount();
+  }
+
+  public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author) {
     return getDraftComments(author, null);
   }
 
   public ImmutableListMultimap<RevId, Comment> getDraftComments(
-      Account.Id author, @Nullable Ref ref) throws OrmException {
+      Account.Id author, @Nullable Ref ref) {
     loadDraftComments(author, ref);
     // Filter out any zombie draft comments. These are drafts that are also in
     // the published map, and arise when the update to All-Users to delete them
@@ -435,7 +437,7 @@
             draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
   }
 
-  public ImmutableListMultimap<RevId, RobotComment> getRobotComments() throws OrmException {
+  public ImmutableListMultimap<RevId, RobotComment> getRobotComments() {
     loadRobotComments();
     return robotCommentNotes.getComments();
   }
@@ -445,14 +447,14 @@
    * However, this method will load the comments if no draft comments have been loaded or if the
    * caller would like the drafts for another author.
    */
-  private void loadDraftComments(Account.Id author, @Nullable Ref ref) throws OrmException {
+  private void loadDraftComments(Account.Id author, @Nullable Ref ref) {
     if (draftCommentNotes == null || !author.equals(draftCommentNotes.getAuthor()) || ref != null) {
       draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author, ref);
       draftCommentNotes.load();
     }
   }
 
-  private void loadRobotComments() throws OrmException {
+  private void loadRobotComments() {
     if (robotCommentNotes == null) {
       robotCommentNotes = new RobotCommentNotes(args, change);
       robotCommentNotes.load();
@@ -468,7 +470,7 @@
     return robotCommentNotes;
   }
 
-  public boolean containsComment(Comment c) throws OrmException {
+  public boolean containsComment(Comment c) {
     if (containsCommentPublished(c)) {
       return true;
     }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index e2af855..1e48907 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -112,8 +112,11 @@
     // Single pointer overhead.
     private static final int P = 8;
 
+    // Single int overhead.
+    private static final int I = 4;
+
     // Single IntKey overhead.
-    private static final int K = O + 4;
+    private static final int K = O + I;
 
     // Single Timestamp overhead.
     private static final int T = O + 8;
@@ -173,7 +176,8 @@
           + map(state.publishedComments().asMap(), comment())
           + 1 // isPrivate
           + 1 // workInProgress
-          + 1; // reviewStarted
+          + 1 // reviewStarted
+          + I; // updateCount
     }
 
     private static int ptr(Object o, int size) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index f807dd6..1e7005b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -166,6 +166,7 @@
   private ReviewerSet pendingReviewers;
   private ReviewerByEmailSet pendingReviewersByEmail;
   private Change.Id revertOf;
+  private int updateCount;
 
   ChangeNotesParser(
       Change.Id changeId,
@@ -264,7 +265,8 @@
         firstNonNull(isPrivate, false),
         firstNonNull(workInProgress, false),
         firstNonNull(hasReviewStarted, true),
-        revertOf);
+        revertOf,
+        updateCount);
   }
 
   private PatchSet.Id buildCurrentPatchSetId() {
@@ -311,6 +313,7 @@
   }
 
   private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
+    updateCount++;
     Timestamp ts = new Timestamp(commit.getCommitterIdent().getWhen().getTime());
 
     createdOn = ts;
@@ -722,7 +725,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);
       }
     }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 4467401..3a983be 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -121,7 +121,8 @@
       boolean isPrivate,
       boolean workInProgress,
       boolean reviewStarted,
-      @Nullable Change.Id revertOf) {
+      @Nullable Change.Id revertOf,
+      int updateCount) {
     requireNonNull(
         metaId,
         () ->
@@ -165,6 +166,7 @@
         .submitRecords(submitRecords)
         .changeMessages(changeMessages)
         .publishedComments(publishedComments)
+        .updateCount(updateCount)
         .build();
   }
 
@@ -297,6 +299,8 @@
 
   abstract ImmutableListMultimap<RevId, Comment> publishedComments();
 
+  abstract int updateCount();
+
   Change newChange(Project.NameKey project) {
     ChangeColumns c = requireNonNull(columns(), "columns are required");
     Change change =
@@ -363,7 +367,8 @@
           .reviewerUpdates(ImmutableList.of())
           .submitRecords(ImmutableList.of())
           .changeMessages(ImmutableList.of())
-          .publishedComments(ImmutableListMultimap.of());
+          .publishedComments(ImmutableListMultimap.of())
+          .updateCount(0);
     }
 
     abstract Builder metaId(ObjectId metaId);
@@ -398,6 +403,8 @@
 
     abstract Builder publishedComments(ListMultimap<RevId, Comment> publishedComments);
 
+    abstract Builder updateCount(int updateCount);
+
     abstract ChangeNotesState build();
   }
 
@@ -459,6 +466,7 @@
           .changeMessages()
           .forEach(m -> b.addChangeMessage(toByteString(m, ChangeMessageProtoConverter.INSTANCE)));
       object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
+      b.setUpdateCount(object.updateCount());
 
       return Protos.toByteArray(b.build());
     }
@@ -543,23 +551,17 @@
               .changeId(changeId)
               .columns(toChangeColumns(changeId, proto.getColumns()))
               .pastAssignees(
-                  proto
-                      .getPastAssigneeList()
-                      .stream()
+                  proto.getPastAssigneeList().stream()
                       .map(Account.Id::new)
                       .collect(toImmutableSet()))
               .hashtags(proto.getHashtagList())
               .patchSets(
-                  proto
-                      .getPatchSetList()
-                      .stream()
+                  proto.getPatchSetList().stream()
                       .map(bytes -> parseProtoFrom(PatchSetProtoConverter.INSTANCE, bytes))
                       .map(ps -> Maps.immutableEntry(ps.getId(), ps))
                       .collect(toImmutableList()))
               .approvals(
-                  proto
-                      .getApprovalList()
-                      .stream()
+                  proto.getApprovalList().stream()
                       .map(bytes -> parseProtoFrom(PatchSetApprovalProtoConverter.INSTANCE, bytes))
                       .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
                       .collect(toImmutableList()))
@@ -568,30 +570,23 @@
               .pendingReviewers(toReviewerSet(proto.getPendingReviewerList()))
               .pendingReviewersByEmail(toReviewerByEmailSet(proto.getPendingReviewerByEmailList()))
               .allPastReviewers(
-                  proto
-                      .getPastReviewerList()
-                      .stream()
+                  proto.getPastReviewerList().stream()
                       .map(Account.Id::new)
                       .collect(toImmutableList()))
               .reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
               .submitRecords(
-                  proto
-                      .getSubmitRecordList()
-                      .stream()
+                  proto.getSubmitRecordList().stream()
                       .map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
                       .collect(toImmutableList()))
               .changeMessages(
-                  proto
-                      .getChangeMessageList()
-                      .stream()
+                  proto.getChangeMessageList().stream()
                       .map(bytes -> parseProtoFrom(ChangeMessageProtoConverter.INSTANCE, bytes))
                       .collect(toImmutableList()))
               .publishedComments(
-                  proto
-                      .getPublishedCommentList()
-                      .stream()
+                  proto.getPublishedCommentList().stream()
                       .map(r -> GSON.fromJson(r, Comment.class))
-                      .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)));
+                      .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)))
+              .updateCount(proto.getUpdateCount());
       return b.build();
     }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 1e2bb5d..b218d9b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -49,8 +49,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
 import com.google.common.collect.TreeBasedTable;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -65,7 +65,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -102,18 +101,8 @@
  */
 public class ChangeUpdate extends AbstractChangeUpdate {
   public interface Factory {
-    ChangeUpdate create(ChangeNotes notes, CurrentUser user);
-
     ChangeUpdate create(ChangeNotes notes, CurrentUser user, Date when);
 
-    ChangeUpdate create(
-        Change change,
-        @Assisted("effective") @Nullable Account.Id accountId,
-        @Assisted("real") @Nullable Account.Id realAccountId,
-        PersonIdent authorIdent,
-        Date when,
-        Comparator<String> labelNameComparator);
-
     @VisibleForTesting
     ChangeUpdate create(
         ChangeNotes notes, CurrentUser user, Date when, Comparator<String> labelNameComparator);
@@ -167,30 +156,6 @@
       ProjectCache projectCache,
       @Assisted ChangeNotes notes,
       @Assisted CurrentUser user,
-      ChangeNoteUtil noteUtil) {
-    this(
-        serverIdent,
-        updateManagerFactory,
-        draftUpdateFactory,
-        robotCommentUpdateFactory,
-        deleteCommentRewriterFactory,
-        projectCache,
-        notes,
-        user,
-        serverIdent.getWhen(),
-        noteUtil);
-  }
-
-  @AssistedInject
-  private ChangeUpdate(
-      @GerritPersonIdent PersonIdent serverIdent,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeDraftUpdate.Factory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
-      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
-      ProjectCache projectCache,
-      @Assisted ChangeNotes notes,
-      @Assisted CurrentUser user,
       @Assisted Date when,
       ChangeNoteUtil noteUtil) {
     this(
@@ -231,29 +196,7 @@
     this.approvals = approvals(labelNameComparator);
   }
 
-  @AssistedInject
-  private ChangeUpdate(
-      @GerritPersonIdent PersonIdent serverIdent,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeDraftUpdate.Factory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
-      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
-      ChangeNoteUtil noteUtil,
-      @Assisted Change change,
-      @Assisted("effective") @Nullable Account.Id accountId,
-      @Assisted("real") @Nullable Account.Id realAccountId,
-      @Assisted PersonIdent authorIdent,
-      @Assisted Date when,
-      @Assisted Comparator<String> labelNameComparator) {
-    super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
-    this.draftUpdateFactory = draftUpdateFactory;
-    this.robotCommentUpdateFactory = robotCommentUpdateFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
-    this.approvals = approvals(labelNameComparator);
-  }
-
-  public ObjectId commit() throws IOException, OrmException {
+  public ObjectId commit() throws IOException {
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
       updateManager.add(this);
       updateManager.execute();
@@ -307,11 +250,6 @@
     checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time");
   }
 
-  @Deprecated // Only until we improve ChangeRebuilder to call merge().
-  public void setSubmissionId(String submissionId) {
-    this.submissionId = submissionId;
-  }
-
   public void setSubjectForCommit(String commitSubject) {
     this.commitSubject = commitSubject;
   }
@@ -368,9 +306,9 @@
         deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage);
   }
 
-  public void deleteChangeMessageByRewritingHistory(int targetMessageIdx, String newMessage) {
+  public void deleteChangeMessageByRewritingHistory(String targetMessageId, String newMessage) {
     deleteChangeMessageRewriter =
-        new DeleteChangeMessageRewriter(getChange().getId(), targetMessageIdx, newMessage);
+        new DeleteChangeMessageRewriter(getChange().getId(), targetMessageId, newMessage);
   }
 
   @VisibleForTesting
@@ -486,7 +424,7 @@
 
   /** @return the tree id for the updated tree */
   private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (comments.isEmpty() && pushCert == null) {
       return null;
     }
@@ -513,7 +451,7 @@
   }
 
   private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (curr.equals(ObjectId.zeroId())) {
       return RevisionNoteMap.emptyMap();
     }
@@ -539,12 +477,11 @@
   }
 
   private void checkComments(
-      Map<RevId, ChangeRevisionNote> existingNotes, Map<RevId, RevisionNoteBuilder> toUpdate)
-      throws OrmException {
+      Map<RevId, ChangeRevisionNote> existingNotes, Map<RevId, RevisionNoteBuilder> toUpdate) {
     // Prohibit various kinds of illegal operations on comments.
     Set<Comment.Key> existing = new HashSet<>();
     for (ChangeRevisionNote rn : existingNotes.values()) {
-      for (Comment c : rn.getComments()) {
+      for (Comment c : rn.getEntities()) {
         existing.add(c.key);
         if (draftUpdate != null) {
           // Take advantage of an existing update on All-Users to prune any
@@ -570,7 +507,7 @@
     for (RevisionNoteBuilder b : toUpdate.values()) {
       for (Comment c : b.put.values()) {
         if (existing.contains(c.key)) {
-          throw new OrmException("Cannot update existing published comment: " + c);
+          throw new StorageException("Cannot update existing published comment: " + c);
         }
       }
     }
@@ -582,8 +519,14 @@
   }
 
   @Override
+  protected boolean bypassMaxUpdates() {
+    // Allow abandoning or submitting a change even if it would exceed the max update count.
+    return status != null && status.isClosed();
+  }
+
+  @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     checkState(
         deleteCommentRewriter == null && deleteChangeMessageRewriter == null,
         "cannot update and rewrite ref in one BatchUpdate");
@@ -738,7 +681,7 @@
         cb.setTreeId(treeId);
       }
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
     return cb;
   }
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
deleted file mode 100644
index 507b45c..0000000
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ /dev/null
@@ -1,255 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.UsedAt;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.PackInserter;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.MutableInteger;
-
-@UsedAt(UsedAt.Project.GOOGLE)
-@Singleton
-public class CommentJsonMigrator {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static class ProjectMigrationResult {
-    public int skipped;
-    public boolean ok;
-    public List<String> refsUpdated;
-  }
-
-  private final LegacyChangeNoteRead legacyChangeNoteRead;
-  private final ChangeNoteJson changeNoteJson;
-  private final AllUsersName allUsers;
-
-  @Inject
-  CommentJsonMigrator(
-      ChangeNoteJson changeNoteJson,
-      GerritServerIdProvider gerritServerIdProvider,
-      AllUsersName allUsers) {
-    this.changeNoteJson = changeNoteJson;
-    this.allUsers = allUsers;
-    this.legacyChangeNoteRead = new LegacyChangeNoteRead(gerritServerIdProvider.get());
-  }
-
-  CommentJsonMigrator(ChangeNoteJson changeNoteJson, String serverId, AllUsersName allUsers) {
-    this.changeNoteJson = changeNoteJson;
-    this.legacyChangeNoteRead = new LegacyChangeNoteRead(serverId);
-    this.allUsers = allUsers;
-  }
-
-  public ProjectMigrationResult migrateProject(
-      Project.NameKey project, Repository repo, boolean dryRun) {
-    ProjectMigrationResult progress = new ProjectMigrationResult();
-    progress.ok = true;
-    progress.skipped = 0;
-    progress.refsUpdated = ImmutableList.of();
-    try (RevWalk rw = new RevWalk(repo);
-        ObjectInserter ins = newPackInserter(repo)) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      bru.setAllowNonFastForwards(true);
-      progress.ok &= migrateChanges(project, repo, rw, ins, bru);
-      if (project.equals(allUsers)) {
-        progress.ok &= migrateDrafts(allUsers, repo, rw, ins, bru);
-      }
-
-      progress.refsUpdated =
-          bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
-      if (!bru.getCommands().isEmpty()) {
-        if (!dryRun) {
-          ins.flush();
-          RefUpdateUtil.executeChecked(bru, rw);
-        }
-      } else {
-        progress.skipped++;
-      }
-    } catch (IOException e) {
-      progress.ok = false;
-    }
-
-    return progress;
-  }
-
-  private boolean migrateChanges(
-      Project.NameKey project, Repository repo, RevWalk rw, ObjectInserter ins, BatchRefUpdate bru)
-      throws IOException {
-    boolean ok = true;
-    for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
-      Change.Id changeId = Change.Id.fromRef(ref.getName());
-      if (changeId == null || !ref.getName().equals(RefNames.changeMetaRef(changeId))) {
-        continue;
-      }
-      ok &= migrateOne(project, rw, ins, bru, Status.PUBLISHED, changeId, ref);
-    }
-    return ok;
-  }
-
-  private boolean migrateDrafts(
-      Project.NameKey allUsers,
-      Repository allUsersRepo,
-      RevWalk rw,
-      ObjectInserter ins,
-      BatchRefUpdate bru)
-      throws IOException {
-    boolean ok = true;
-    for (Ref ref : allUsersRepo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
-      Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
-      if (changeId == null) {
-        continue;
-      }
-      ok &= migrateOne(allUsers, rw, ins, bru, Status.DRAFT, changeId, ref);
-    }
-    return ok;
-  }
-
-  private boolean migrateOne(
-      Project.NameKey project,
-      RevWalk rw,
-      ObjectInserter ins,
-      BatchRefUpdate bru,
-      Status status,
-      Change.Id changeId,
-      Ref ref) {
-    ObjectId oldId = ref.getObjectId();
-    try {
-      if (!hasAnyLegacyComments(rw, oldId)) {
-        return true;
-      }
-    } catch (IOException e) {
-      logger.atInfo().log(
-          String.format(
-              "Error reading change %s in %s; attempting migration anyway", changeId, project),
-          e);
-    }
-
-    try {
-      reset(rw, oldId);
-
-      ObjectReader reader = rw.getObjectReader();
-      ObjectId newId = null;
-      RevCommit c;
-      while ((c = rw.next()) != null) {
-        CommitBuilder cb = new CommitBuilder();
-        cb.setAuthor(c.getAuthorIdent());
-        cb.setCommitter(c.getCommitterIdent());
-        cb.setMessage(c.getFullMessage());
-        cb.setEncoding(c.getEncoding());
-        if (newId != null) {
-          cb.setParentId(newId);
-        }
-
-        // Read/write using the low-level RevisionNote API, which works regardless of NotesMigration
-        // state.
-        NoteMap noteMap = NoteMap.read(reader, c);
-        RevisionNoteMap<ChangeRevisionNote> revNoteMap =
-            RevisionNoteMap.parse(
-                changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, status);
-        RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNoteMap);
-
-        for (RevId revId : revNoteMap.revisionNotes.keySet()) {
-          // Call cache.get on each known RevId to read the old note in whichever format, then write
-          // the note in JSON format.
-          byte[] data = cache.get(revId).build(changeNoteJson);
-          noteMap.set(ObjectId.fromString(revId.get()), ins.insert(OBJ_BLOB, data));
-        }
-        cb.setTreeId(noteMap.writeTree(ins));
-        newId = ins.insert(cb);
-      }
-
-      bru.addCommand(new ReceiveCommand(oldId, newId, ref.getName()));
-      return true;
-    } catch (ConfigInvalidException | IOException e) {
-      logger.atInfo().log(String.format("Error migrating change %s in %s", changeId, project), e);
-      return false;
-    }
-  }
-
-  private static boolean hasAnyLegacyComments(RevWalk rw, ObjectId id) throws IOException {
-    ObjectReader reader = rw.getObjectReader();
-    reset(rw, id);
-
-    // Check the note map at each commit, not just the tip. It's possible that the server switched
-    // from legacy to JSON partway through its history, which would have mixed legacy/JSON comments
-    // in its history. Although the tip commit would continue to parse once we remove the legacy
-    // parser, our goal is really to expunge all vestiges of the old format, which implies rewriting
-    // history (and thus returning true) in this case.
-    RevCommit c;
-    while ((c = rw.next()) != null) {
-      NoteMap noteMap = NoteMap.read(reader, c);
-      for (Note note : noteMap) {
-        // Match pre-parsing logic in RevisionNote#parse().
-        ObjectLoader objectLoader = reader.open(note.getData(), OBJ_BLOB);
-        if (objectLoader.isLarge()) {
-          throw new IOException(String.format("Comment note %s is too large", note.name()));
-        }
-        byte[] raw = objectLoader.getCachedBytes();
-        MutableInteger p = new MutableInteger();
-        RevisionNote.trimLeadingEmptyLines(raw, p);
-        if (!ChangeRevisionNote.isJson(raw, p.value)) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private static void reset(RevWalk rw, ObjectId id) throws IOException {
-    rw.reset();
-    rw.sort(RevSort.TOPO);
-    rw.sort(RevSort.REVERSE);
-    rw.markStart(rw.parseCommit(id));
-  }
-
-  private static ObjectInserter newPackInserter(Repository repo) {
-    if (!(repo instanceof FileRepository)) {
-      return repo.newObjectInserter();
-    }
-    PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
-    ins.checkExisting(false);
-    return ins;
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
index 212aa37..8fb28e1 100644
--- a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
 import static org.eclipse.jgit.util.RawParseUtils.decode;
@@ -40,12 +41,12 @@
 public class DeleteChangeMessageRewriter implements NoteDbRewriter {
 
   private final Change.Id changeId;
-  private final int targetMessageIdx;
+  private final String targetMessageId;
   private final String newChangeMessage;
 
-  DeleteChangeMessageRewriter(Change.Id changeId, int targetMessageIdx, String newChangeMessage) {
+  DeleteChangeMessageRewriter(Change.Id changeId, String targetMessageId, String newChangeMessage) {
     this.changeId = changeId;
-    this.targetMessageIdx = targetMessageIdx;
+    this.targetMessageId = checkNotNull(targetMessageId);
     this.newChangeMessage = newChangeMessage;
   }
 
@@ -67,21 +68,18 @@
 
     ObjectId newTipId = null;
     RevCommit originalCommit;
-    int idx = 0;
+    boolean startRewrite = false;
     while ((originalCommit = revWalk.next()) != null) {
-      if (idx < targetMessageIdx) {
+      boolean isTargetCommit = originalCommit.getId().getName().equals(targetMessageId);
+      if (!startRewrite && !isTargetCommit) {
         newTipId = originalCommit;
-        idx++;
         continue;
       }
 
+      startRewrite = true;
       String newCommitMessage =
-          (idx == targetMessageIdx)
-              ? createNewCommitMessage(originalCommit)
-              : originalCommit.getFullMessage();
+          isTargetCommit ? createNewCommitMessage(originalCommit) : originalCommit.getFullMessage();
       newTipId = rewriteOneCommit(originalCommit, newTipId, newCommitMessage, inserter);
-
-      idx++;
     }
     return newTipId;
   }
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 0cd3452..c100550 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -86,7 +85,7 @@
 
   @Override
   public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException {
     checkArgument(!currTip.equals(ObjectId.zeroId()));
 
     // Walk from the first commit of the branch.
@@ -141,10 +140,8 @@
       throws IOException, ConfigInvalidException {
     return RevisionNoteMap.parse(
             changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, PUBLISHED)
-        .revisionNotes
-        .values()
-        .stream()
-        .flatMap(n -> n.getComments().stream())
+        .revisionNotes.values().stream()
+        .flatMap(n -> n.getEntities().stream())
         .collect(toMap(c -> c.key.uuid, Function.identity()));
   }
 
@@ -189,9 +186,7 @@
    */
   private List<Comment> getDeletedComments(
       Map<String, Comment> parMap, Map<String, Comment> curMap) {
-    return parMap
-        .entrySet()
-        .stream()
+    return parMap.entrySet().stream()
         .filter(c -> !curMap.containsKey(c.getKey()))
         .map(Map.Entry::getValue)
         .collect(toList());
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 2523c2c..213613e 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -130,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);
       }
     }
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
index f8c713c..6305a54 100644
--- a/java/com/google/gerrit/server/notedb/IntBlob.java
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -22,10 +22,10 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Optional;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -41,20 +41,19 @@
 
 @AutoValue
 public abstract class IntBlob {
-  public static Optional<IntBlob> parse(Repository repo, String refName)
-      throws IOException, OrmException {
+  public static Optional<IntBlob> parse(Repository repo, String refName) throws IOException {
     try (ObjectReader or = repo.newObjectReader()) {
       return parse(repo, refName, or);
     }
   }
 
   public static Optional<IntBlob> parse(Repository repo, String refName, RevWalk rw)
-      throws IOException, OrmException {
+      throws IOException {
     return parse(repo, refName, rw.getObjectReader());
   }
 
   private static Optional<IntBlob> parse(Repository repo, String refName, ObjectReader or)
-      throws IOException, OrmException {
+      throws IOException {
     Ref ref = repo.exactRef(refName);
     if (ref == null) {
       return Optional.empty();
@@ -69,7 +68,7 @@
     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());
+      throw new StorageException("invalid value in " + refName + " blob at " + id.name());
     }
     return Optional.of(IntBlob.create(id, 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/NoteDbRewriter.java b/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
index 3c7b0a3..19754d1 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -35,5 +34,5 @@
    * @return the {@code ObjectId} of the ref's new tip commit.
    */
   ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
-      throws IOException, ConfigInvalidException, OrmException;
+      throws IOException, ConfigInvalidException;
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 7f23f87..5f5eb67 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -20,11 +20,13 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.Timer1;
@@ -33,12 +35,12 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.InsertedObject;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -46,10 +48,12 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -80,6 +84,23 @@
     NoteDbUpdateManager create(Project.NameKey projectName);
   }
 
+  public static class TooManyUpdatesException extends StorageException {
+    @VisibleForTesting
+    public static String message(Change.Id id, int maxUpdates) {
+      return "Change "
+          + id
+          + " may not exceed "
+          + maxUpdates
+          + " updates. It may still be abandoned or submitted.";
+    }
+
+    private static final long serialVersionUID = 1L;
+
+    private TooManyUpdatesException(Change.Id id, int maxUpdates) {
+      super(message(id, maxUpdates));
+    }
+  }
+
   public static class OpenRepo implements AutoCloseable {
     public final Repository repo;
     public final RevWalk rw;
@@ -149,6 +170,7 @@
   private final AllUsersName allUsersName;
   private final NoteDbMetrics metrics;
   private final Project.NameKey projectName;
+  private final int maxUpdates;
   private final ListMultimap<String, ChangeUpdate> changeUpdates;
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
   private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
@@ -164,6 +186,7 @@
 
   @Inject
   NoteDbUpdateManager(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
@@ -174,6 +197,7 @@
     this.allUsersName = allUsersName;
     this.metrics = metrics;
     this.projectName = projectName;
+    maxUpdates = cfg.getInt("change", null, "maxUpdates", 1000);
     changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
@@ -348,10 +372,9 @@
   /**
    * Stage updates in the manager's internal list of commands.
    *
-   * @throws OrmException if a database layer error occurs.
    * @throws IOException if a storage layer error occurs.
    */
-  private void stage() throws OrmException, IOException {
+  private void stage() throws IOException {
     try (Timer1.Context timer = metrics.stageUpdateLatency.start(CHANGES)) {
       if (isEmpty()) {
         return;
@@ -376,12 +399,12 @@
   }
 
   @Nullable
-  public BatchRefUpdate execute() throws OrmException, IOException {
+  public BatchRefUpdate execute() throws IOException {
     return execute(false);
   }
 
   @Nullable
-  public BatchRefUpdate execute(boolean dryrun) throws OrmException, IOException {
+  public BatchRefUpdate execute(boolean dryrun) throws IOException {
     checkNotExecuted();
     if (isEmpty()) {
       executed = true;
@@ -476,7 +499,7 @@
     return -1;
   }
 
-  private void addCommands() throws OrmException, IOException {
+  private void addCommands() throws IOException {
     if (isEmpty()) {
       return;
     }
@@ -484,12 +507,12 @@
     if (!draftUpdates.isEmpty()) {
       checkState(allUsersRepo != null, "must set all users repo");
     }
-    addUpdates(changeUpdates, changeRepo);
+    addUpdates(changeUpdates, changeRepo, Optional.of(maxUpdates));
     if (!draftUpdates.isEmpty()) {
-      addUpdates(draftUpdates, allUsersRepo);
+      addUpdates(draftUpdates, allUsersRepo, Optional.empty());
     }
     if (!robotCommentUpdates.isEmpty()) {
-      addUpdates(robotCommentUpdates, changeRepo);
+      addUpdates(robotCommentUpdates, changeRepo, Optional.empty());
     }
     if (!rewriters.isEmpty()) {
       addRewrites(rewriters, changeRepo);
@@ -522,7 +545,7 @@
   }
 
   private static <U extends AbstractChangeUpdate> void addUpdates(
-      ListMultimap<String, U> all, OpenRepo or) throws OrmException, IOException {
+      ListMultimap<String, U> all, OpenRepo or, Optional<Integer> maxUpdates) throws IOException {
     for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
       String refName = e.getKey();
       Collection<U> updates = e.getValue();
@@ -534,15 +557,30 @@
         continue;
       }
 
+      int updateCount;
+      U first = updates.iterator().next();
+      if (maxUpdates.isPresent()) {
+        checkState(first.getNotes() != null, "expected ChangeNotes on %s", first);
+        updateCount = first.getNotes().getUpdateCount();
+      } else {
+        updateCount = 0;
+      }
+
       ObjectId curr = old;
       for (U u : updates) {
         if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) {
-          throw new OrmException("Given ChangeUpdate is only allowed on initial commit");
+          throw new StorageException("Given ChangeUpdate is only allowed on initial commit");
         }
         ObjectId next = u.apply(or.rw, or.tempIns, curr);
         if (next == null) {
           continue;
         }
+        if (maxUpdates.isPresent()
+            && !Objects.equals(next, curr)
+            && ++updateCount > maxUpdates.get()
+            && !u.bypassMaxUpdates()) {
+          throw new TooManyUpdatesException(u.getId(), maxUpdates.get());
+        }
         curr = next;
       }
       if (!old.equals(curr)) {
@@ -552,13 +590,13 @@
   }
 
   private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo)
-      throws OrmException, IOException {
+      throws IOException {
     for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) {
       String refName = entry.getKey();
       ObjectId oldTip = openRepo.cmds.get(refName).orElse(ObjectId.zeroId());
 
       if (oldTip.equals(ObjectId.zeroId())) {
-        throw new OrmException(String.format("Ref %s is empty", refName));
+        throw new StorageException(String.format("Ref %s is empty", refName));
       }
 
       ObjectId currTip = oldTip;
@@ -571,7 +609,7 @@
           }
         }
       } catch (ConfigInvalidException e) {
-        throw new OrmException("Cannot rewrite commit history", e);
+        throw new StorageException("Cannot rewrite commit history", e);
       }
 
       if (!oldTip.equals(currTip)) {
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 56264e9..3919622 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -30,12 +30,12 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -65,7 +65,7 @@
 public class RepoSequence {
   @FunctionalInterface
   public interface Seed {
-    int get() throws OrmException;
+    int get();
   }
 
   @VisibleForTesting
@@ -184,7 +184,7 @@
     counterLock = new ReentrantLock(true);
   }
 
-  public int next() throws OrmException {
+  public int next() {
     counterLock.lock();
     try {
       if (counter >= limit) {
@@ -196,7 +196,7 @@
     }
   }
 
-  public ImmutableList<Integer> next(int count) throws OrmException {
+  public ImmutableList<Integer> next(int count) {
     if (count == 0) {
       return ImmutableList.of();
     }
@@ -221,7 +221,7 @@
   }
 
   @VisibleForTesting
-  public void set(int val) throws OrmException {
+  public void set(int val) {
     // Don't bother spinning. This is only for tests, and a test that calls set
     // concurrently with other writes is doing it wrong.
     counterLock.lock();
@@ -231,14 +231,14 @@
         IntBlob.store(repo, rw, projectName, refName, null, val, gitRefUpdated);
         counter = limit;
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     } finally {
       counterLock.unlock();
     }
   }
 
-  public void increaseTo(int val) throws OrmException {
+  public void increaseTo(int val) {
     counterLock.lock();
     try {
       try (Repository repo = repoManager.openRepository(projectName);
@@ -252,18 +252,18 @@
         counter = limit;
       } catch (ExecutionException | RetryException e) {
         if (e.getCause() != null) {
-          Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+          Throwables.throwIfInstanceOf(e.getCause(), StorageException.class);
         }
-        throw new OrmException(e);
+        throw new StorageException(e);
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     } finally {
       counterLock.unlock();
     }
   }
 
-  private void acquire(int count) throws OrmException {
+  private void acquire(int count) {
     try (Repository repo = repoManager.openRepository(projectName);
         RevWalk rw = new RevWalk(repo)) {
       TryAcquire attempt = new TryAcquire(repo, rw, count);
@@ -273,11 +273,11 @@
       acquireCount++;
     } catch (ExecutionException | RetryException e) {
       if (e.getCause() != null) {
-        Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), StorageException.class);
       }
-      throw new OrmException(e);
+      throw new StorageException(e);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
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/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index 364ad75..92dd7d8 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -97,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 9307724..0304ab8 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -19,13 +19,13 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -101,7 +101,7 @@
 
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     RevisionNoteMap<RobotCommentsRevisionNote> rnm = getRevisionNoteMap(rw, curr);
     Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
@@ -148,7 +148,7 @@
   }
 
   private RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, OrmException, IOException {
+      throws ConfigInvalidException, IOException {
     if (curr.equals(ObjectId.zeroId())) {
       return RevisionNoteMap.emptyMap();
     }
@@ -179,13 +179,13 @@
 
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws OrmException, IOException {
+      throws IOException {
     CommitBuilder cb = new CommitBuilder();
     cb.setMessage("Update robot comments");
     try {
       return storeCommentsInNotes(rw, ins, curr, cb);
     } catch (ConfigInvalidException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
similarity index 91%
rename from java/com/google/gerrit/server/Sequences.java
rename to java/com/google/gerrit/server/notedb/Sequences.java
index 7d98bc4..9e8c541 100644
--- a/java/com/google/gerrit/server/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.server.notedb;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.metrics.Description;
@@ -25,8 +25,6 @@
 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.RepoSequence;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
@@ -101,25 +99,25 @@
             Field.ofBoolean("multiple"));
   }
 
-  public int nextAccountId() throws OrmException {
+  public int nextAccountId() {
     try (Timer2.Context timer = nextIdLatency.start(SequenceType.ACCOUNTS, false)) {
       return accountSeq.next();
     }
   }
 
-  public int nextChangeId() throws OrmException {
+  public int nextChangeId() {
     try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
       return changeSeq.next();
     }
   }
 
-  public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
+  public ImmutableList<Integer> nextChangeIds(int count) {
     try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
       return changeSeq.next(count);
     }
   }
 
-  public int nextGroupId() throws OrmException {
+  public int nextGroupId() {
     try (Timer2.Context timer = nextIdLatency.start(SequenceType.GROUPS, false)) {
       return groupSeq.next();
     }
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/EditTransformer.java b/java/com/google/gerrit/server/patch/EditTransformer.java
index 9083ede..90f442e 100644
--- a/java/com/google/gerrit/server/patch/EditTransformer.java
+++ b/java/com/google/gerrit/server/patch/EditTransformer.java
@@ -89,8 +89,7 @@
    * @return the transformed edits per file path
    */
   public Multimap<String, ContextAwareEdit> getEditsPerFilePath() {
-    return edits
-        .stream()
+    return edits.stream()
         .collect(
             toMultimap(
                 ContextAwareEdit::getNewFilePath, Function.identity(), ArrayListMultimap::create));
@@ -112,9 +111,7 @@
         transformingEntries.stream().collect(groupingBy(EditTransformer::getOldFilePath));
 
     edits =
-        editsPerFilePath
-            .entrySet()
-            .stream()
+        editsPerFilePath.entrySet().stream()
             .flatMap(
                 pathAndEdits -> {
                   List<PatchListEntry> transEntries =
@@ -137,12 +134,11 @@
     }
 
     // TODO(aliceks): Find a way to prevent an explosion of the number of entries.
-    return transformingEntries
-        .stream()
+    return transformingEntries.stream()
         .flatMap(
             transEntry ->
                 transformEdits(
-                        sideStrategy, originalEdits, transEntry.getEdits(), transEntry.getNewName())
+                    sideStrategy, originalEdits, transEntry.getEdits(), transEntry.getNewName())
                     .stream());
   }
 
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index aff519a..1593b11 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -16,7 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.exceptions.NoSuchEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 import java.io.IOException;
 import org.eclipse.jgit.errors.CorruptObjectException;
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 074e344..08de537 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -287,8 +287,7 @@
     }
 
     List<DiffEntry> relevantDiffEntries =
-        diffEntries
-            .stream()
+        diffEntries.stream()
             .filter(diffEntry -> isTouched(touchedFilePaths, diffEntry))
             .collect(toImmutableList());
 
@@ -397,8 +396,7 @@
   }
 
   private static Set<Edit> getContentEdits(Set<ContextAwareEdit> editsDueToRebase) {
-    return editsDueToRebase
-        .stream()
+    return editsDueToRebase.stream()
         .map(ContextAwareEdit::toEdit)
         .filter(Optional::isPresent)
         .map(Optional::get)
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 50d3711..08ee7a3 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -187,8 +186,8 @@
 
   @Override
   public PatchScript call()
-      throws OrmException, LargeObjectException, AuthException, InvalidChangeOperationException,
-          IOException, PermissionBackendException {
+      throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException,
+          PermissionBackendException {
     if (parentNum < 0) {
       validatePatchSetId(psa);
     }
@@ -259,7 +258,7 @@
     return b;
   }
 
-  private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException, OrmException {
+  private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException {
     if (ps.getId().get() == 0) {
       return getEditRev();
     }
@@ -275,7 +274,7 @@
     }
   }
 
-  private ObjectId getEditRev() throws AuthException, IOException, OrmException {
+  private ObjectId getEditRev() throws AuthException, IOException {
     edit = editReader.byChange(notes);
     if (edit.isPresent()) {
       return edit.get().getEditCommit();
@@ -291,8 +290,7 @@
     }
   }
 
-  private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName)
-      throws OrmException {
+  private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName) {
     Map<Patch.Key, Patch> byKey = new HashMap<>();
 
     if (loadHistory) {
@@ -384,7 +382,7 @@
     }
   }
 
-  private void loadPublished(Map<Patch.Key, Patch> byKey, String file) throws OrmException {
+  private void loadPublished(Map<Patch.Key, Patch> byKey, String file) {
     for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) {
       comments.include(notes.getChangeId(), c);
       PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
@@ -396,8 +394,7 @@
     }
   }
 
-  private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file)
-      throws OrmException {
+  private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) {
     for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) {
       comments.include(notes.getChangeId(), c);
       PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
diff --git a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index f11f116..0eb5588 100644
--- a/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -53,8 +53,7 @@
     this.emails = emails;
   }
 
-  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi)
-      throws IOException, OrmException {
+  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi) throws IOException {
     rw.parseBody(src);
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
@@ -70,7 +69,7 @@
     try {
       PatchSet patchSet = psUtil.get(notes, psId);
       return get(notes.getProjectName(), patchSet);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
   }
@@ -83,13 +82,13 @@
       PatchSetInfo info = get(rw, src, patchSet.getId());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
   }
 
   // TODO: The same method exists in EventFactory, find a common place for it
-  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException {
     final UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 5e9501c..ee362002 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -31,7 +32,6 @@
 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.Singleton;
 import java.util.Collection;
@@ -52,8 +52,7 @@
       this.notesFactory = notesFactory;
     }
 
-    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId)
-        throws OrmException {
+    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId) {
       return create(refControl, notesFactory.create(project, changeId));
     }
 
@@ -90,7 +89,7 @@
   }
 
   /** Can this user see this change? */
-  private boolean isVisible(@Nullable ChangeData cd) throws OrmException {
+  private boolean isVisible(@Nullable ChangeData cd) {
     if (getChange().isPrivate() && !isPrivateVisible(cd)) {
       return false;
     }
@@ -154,7 +153,7 @@
   }
 
   /** Is this user a reviewer for the change? */
-  private boolean isReviewer(@Nullable ChangeData cd) throws OrmException {
+  private boolean isReviewer(@Nullable ChangeData cd) {
     if (getUser().isIdentifiedUser()) {
       cd = cd != null ? cd : changeDataFactory.create(notes);
       Collection<Account.Id> results = cd.reviewers().all();
@@ -165,7 +164,7 @@
 
   /** Can this user edit the topic name? */
   private boolean canEditTopicName() {
-    if (getChange().getStatus().isOpen()) {
+    if (getChange().isNew()) {
       return isOwner() // owner (aka creator) of the change can edit topic
           || refControl.isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
@@ -176,9 +175,17 @@
     return refControl.canForceEditTopicName();
   }
 
+  /** Can this user toggle WorkInProgress state? */
+  private boolean canToggleWorkInProgressState() {
+    return isOwner()
+        || getProjectControl().isOwner()
+        || refControl.canPerform(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+        || getProjectControl().isAdmin();
+  }
+
   /** Can this user edit the description? */
   private boolean canEditDescription() {
-    if (getChange().getStatus().isOpen()) {
+    if (getChange().isNew()) {
       return isOwner() // owner (aka creator) of the change can edit desc
           || refControl.isOwner() // branch owner can edit desc
           || getProjectControl().isOwner() // project owner can edit desc
@@ -204,7 +211,7 @@
         || getProjectControl().isAdmin();
   }
 
-  private boolean isPrivateVisible(ChangeData cd) throws OrmException {
+  private boolean isPrivateVisible(ChangeData cd) {
     return isOwner()
         || isReviewer(cd)
         || refControl.canPerform(Permission.VIEW_PRIVATE_CHANGES)
@@ -299,12 +306,14 @@
             return canRestore();
           case SUBMIT:
             return refControl.canSubmit(isOwner());
+          case TOGGLE_WORK_IN_PROGRESS_STATE:
+            return canToggleWorkInProgressState();
 
           case REMOVE_REVIEWER:
           case SUBMIT_AS:
             return refControl.canPerform(changePermissionName(perm));
         }
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         throw new PermissionBackendException("unavailable", e);
       }
       throw new PermissionBackendException(perm + " unsupported");
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index ca1c460..2fba4ef 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -55,7 +55,8 @@
    */
   REBASE,
   SUBMIT,
-  SUBMIT_AS("submit on behalf of other users");
+  SUBMIT_AS("submit on behalf of other users"),
+  TOGGLE_WORK_IN_PROGRESS_STATE;
 
   private final String description;
 
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index b3b45d9..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;
@@ -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");
@@ -252,8 +251,7 @@
     private boolean allow(Collection<PermissionRule> rules) {
       return user.getEffectiveGroups()
           .containsAnyOf(
-              rules
-                  .stream()
+              rules.stream()
                   .filter(r -> r.getAction() == Action.ALLOW)
                   .map(r -> r.getGroup().getUUID())
                   .collect(toSet()));
@@ -261,22 +259,11 @@
 
     private boolean notDenied(Collection<PermissionRule> rules) {
       Set<AccountGroup.UUID> denied =
-          rules
-              .stream()
+          rules.stream()
               .filter(r -> r.getAction() != Action.ALLOW)
               .map(r -> r.getGroup().getUUID())
               .collect(toSet());
       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 56c22e1..a5174e1 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -14,15 +14,24 @@
 
 package com.google.gerrit.server.permissions;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toMap;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
@@ -37,24 +46,23 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.ChangeRefCache;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TagMatcher;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -62,13 +70,15 @@
 import org.eclipse.jgit.lib.SymbolicRef;
 
 class DefaultRefFilter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   interface Factory {
     DefaultRefFilter create(ProjectControl projectControl);
   }
 
   private final TagCache tagCache;
   private final ChangeNotes.Factory changeNotesFactory;
-  @Nullable private final ChangeRefCache changeCache;
+  @Nullable private final SearchingChangeCacheImpl changeCache;
   private final GroupCache groupCache;
   private final PermissionBackend permissionBackend;
   private final ProjectControl projectControl;
@@ -78,14 +88,14 @@
   private final Counter0 fullFilterCount;
   private final Counter0 skipFilterCount;
   private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
-  private final Map<Change.Id, Branch.NameKey> visibleChanges;
-  private final Set<Change.Id> inVisibleChanges;
+
+  private Map<Change.Id, Branch.NameKey> visibleChanges;
 
   @Inject
   DefaultRefFilter(
       TagCache tagCache,
       ChangeNotes.Factory changeNotesFactory,
-      @Nullable ChangeRefCache changeCache,
+      @Nullable SearchingChangeCacheImpl changeCache,
       GroupCache groupCache,
       PermissionBackend permissionBackend,
       @GerritServerConfig Config config,
@@ -115,26 +125,72 @@
                     "Rate of ref filter operations where we skip full evaluation"
                         + " because the user can read all refs")
                 .setRate());
-    // TODO(hiesel): Rework who can see change edits so that we can keep just a single set here.
-    this.visibleChanges = new HashMap<>();
-    this.inVisibleChanges = new HashSet<>();
   }
 
+  /** 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() && 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();
@@ -158,8 +214,6 @@
 
     Map<String, Ref> result = new HashMap<>();
     List<Ref> deferredTags = new ArrayList<>();
-    changeCache.bootstrapIfNecessary(projectState.getNameKey());
-
     for (Ref ref : refs.values()) {
       String name = ref.getName();
       Change.Id changeId;
@@ -234,41 +288,26 @@
         }
       }
     }
-
-    // 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(
-                              getAllRefsMap(repo),
-                              repo,
-                              opts.toBuilder().setFilterTagsSeparately(false).build())
-                          .values()
-                      : result.values());
-      for (Ref tag : deferredTags) {
-        try {
-          if (tags.isReachable(tag)) {
-            result.put(tag.getName(), tag);
-          }
-        } catch (IOException e) {
-          throw new PermissionBackendException(e);
-        }
-      }
-    }
-
-    return result;
+    return new AutoValue_DefaultRefFilter_Result(result, deferredTags);
   }
 
-  private static Map<String, Ref> getAllRefsMap(Repository repo) throws PermissionBackendException {
+  /**
+   * 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().collect(toMap(Ref::getName, r -> r));
+      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);
     }
@@ -297,68 +336,115 @@
   }
 
   private boolean visible(Repository repo, Change.Id changeId) throws PermissionBackendException {
-    if (visibleChanges.containsKey(changeId)) {
-      return true;
-    }
-    if (inVisibleChanges.contains(changeId)) {
-      return false;
-    }
-    // TODO(hiesel): The project state should be checked once in the beginning an left alone
-    // thereafter.
-    if (!projectState.statePermitsRead()) {
-      return false;
-    }
-
-    Project.NameKey project = projectState.getNameKey();
-    try {
-      ChangeData cd =
-          changeCache.getChangeData(
-              project, changeId, repo.exactRef(RefNames.changeMetaRef(changeId)).getObjectId());
-      ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
-      try {
-        permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ);
-        visibleChanges.put(cd.getId(), cd.change().getDest());
-        return true;
-      } catch (AuthException e) {
-        inVisibleChanges.add(changeId);
-        return false;
+    if (visibleChanges == null) {
+      if (changeCache == null) {
+        visibleChanges = visibleChangesByScan(repo);
+      } else {
+        visibleChanges = visibleChangesBySearch();
       }
-    } catch (OrmException | IOException e) {
-      throw new PermissionBackendException(
-          "Cannot load change " + changeId + " for project " + project + ", assuming not visible",
-          e);
     }
+    return visibleChanges.containsKey(changeId);
   }
 
   private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException {
     Change.Id id = Change.Id.fromEditRefPart(name);
-    if (id == null) {
-      return false;
+    // Initialize if it wasn't yet
+    if (visibleChanges == null) {
+      visible(repo, id);
     }
-    if (!visible(repo, id)) {
-      // Can't see the change, so can't see the edit.
+    if (id == null) {
       return false;
     }
 
     if (user.isIdentifiedUser()
-        && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))) {
-      // Own edit
+        && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
+        && visible(repo, id)) {
       return true;
     }
+    if (visibleChanges.containsKey(id)) {
+      try {
+        // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
+        permissionBackendForProject
+            .ref(visibleChanges.get(id).get())
+            .check(RefPermission.READ_PRIVATE_CHANGES);
+        return true;
+      } catch (AuthException e) {
+        return false;
+      }
+    }
+    return false;
+  }
 
+  private Map<Change.Id, Branch.NameKey> visibleChangesBySearch()
+      throws PermissionBackendException {
+    Project.NameKey project = projectState.getNameKey();
     try {
-      // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
-      permissionBackendForProject
-          .ref(visibleChanges.get(id).get())
-          .check(RefPermission.READ_PRIVATE_CHANGES);
-      return true;
-    } catch (AuthException e) {
-      return false;
+      Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
+      for (ChangeData cd : changeCache.getChangeData(project)) {
+        ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
+        if (!projectState.statePermitsRead()) {
+          continue;
+        }
+        try {
+          permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ);
+          visibleChanges.put(cd.getId(), cd.change().getDest());
+        } catch (AuthException e) {
+          // Do nothing.
+        }
+      }
+      return visibleChanges;
+    } catch (StorageException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot load changes for project %s, assuming no changes are visible", project);
+      return Collections.emptyMap();
     }
   }
 
+  private Map<Change.Id, Branch.NameKey> visibleChangesByScan(Repository repo)
+      throws PermissionBackendException {
+    Project.NameKey p = projectState.getNameKey();
+    ImmutableList<ChangeNotesResult> changes;
+    try {
+      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);
+      return Collections.emptyMap();
+    }
+
+    Map<Change.Id, Branch.NameKey> result = Maps.newHashMapWithExpectedSize(changes.size());
+    for (ChangeNotesResult notesResult : changes) {
+      ChangeNotes notes = toNotes(notesResult);
+      if (notes != null) {
+        result.put(notes.getChangeId(), notes.getChange().getDest());
+      }
+    }
+    return result;
+  }
+
+  @Nullable
+  private ChangeNotes toNotes(ChangeNotesResult r) throws PermissionBackendException {
+    if (r.error().isPresent()) {
+      logger.atWarning().withCause(r.error().get()).log(
+          "Failed to load change %s in %s", r.id(), projectState.getName());
+      return null;
+    }
+
+    if (!projectState.statePermitsRead()) {
+      return null;
+    }
+
+    try {
+      permissionBackendForProject.change(r.notes()).check(ChangePermission.READ);
+      return r.notes();
+    } catch (AuthException e) {
+      // Skip.
+    }
+    return null;
+  }
+
   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) {
@@ -397,4 +483,46 @@
     return isAdmin
         || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
   }
+
+  /**
+   * Returns true if the user can see the provided change ref. Uses NoteDb for evaluation, hence
+   * does not suffer from the limitations documented in {@link SearchingChangeCacheImpl}.
+   *
+   * <p>This code lets users fetch changes that are not among the fraction of most recently modified
+   * changes that {@link SearchingChangeCacheImpl} returns. This works only when Git Protocol v2
+   * with refs-in-wants is used as that enables Gerrit to skip traditional advertisement of all
+   * visible refs.
+   */
+  private boolean canSeeSingleChangeRef(String refName) throws PermissionBackendException {
+    // We are treating just a single change ref. We are therefore not going through regular ref
+    // filtering, but use NoteDb directly. This makes it so that we can always serve this ref
+    // even if the change is not part of the set of most recent changes that
+    // SearchingChangeCacheImpl returns.
+    Change.Id cId = Change.Id.fromRef(refName);
+    checkNotNull(cId, "invalid change id for ref %s", refName);
+    ChangeNotes notes;
+    try {
+      notes = changeNotesFactory.create(projectState.getNameKey(), cId);
+    } catch (StorageException e) {
+      throw new PermissionBackendException("can't construct change notes", e);
+    }
+    try {
+      permissionBackendForProject.change(notes).check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
+  }
+
+  @AutoValue
+  abstract static class Result {
+    /** Subset of the refs passed into the computation that is visible to the user. */
+    abstract Map<String, Ref> visibleRefs();
+
+    /**
+     * List of tags where we couldn't figure out visibility in the first pass and need to do an
+     * expensive ref walk.
+     */
+    abstract List<Ref> deferredTags();
+  }
 }
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 4affc0b..5c7ee0d 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.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.reviewdb.client.Project;
@@ -124,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");
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index a87eb24..3d10181 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -23,6 +23,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -33,7 +35,6 @@
 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 java.util.Collection;
 import java.util.Collections;
@@ -164,7 +165,7 @@
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest()).change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -280,7 +281,7 @@
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest().get()).change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -300,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) {
@@ -320,7 +326,7 @@
       }
     }
 
-    public abstract BooleanCondition testCond(ProjectPermission perm);
+    public abstract BooleanCondition testCond(CoreOrPluginProjectPermission perm);
 
     /**
      * Filter a map of references by visibility.
@@ -358,9 +364,6 @@
     /** 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}.
@@ -372,7 +375,6 @@
     public static Builder builder() {
       return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
           .setFilterMeta(false)
-          .setFilterTagsSeparately(false)
           .setPrefixes(Collections.singletonList(""));
     }
 
@@ -380,8 +382,6 @@
     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();
@@ -505,9 +505,7 @@
     }
 
     private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
-      return label
-          .getValues()
-          .stream()
+      return label.getValues().stream()
           .map((v) -> new LabelPermission.WithValue(label, v))
           .collect(toSet());
     }
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/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index b419698..1a3198d 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -151,8 +151,7 @@
             Lists.reverse(Lists.newArrayList(sectionToProject.entrySet()));
 
         Map<Project.NameKey, List<AccessSection>> accessByProject =
-            accessDescending
-                .stream()
+            accessDescending.stream()
                 .collect(
                     Collectors.groupingBy(
                         Map.Entry::getValue,
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 e1e7047..e370151 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -17,9 +17,13 @@
 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.exceptions.StorageException;
+import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
+import com.google.gerrit.extensions.api.access.PluginProjectPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -40,12 +44,10 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionMatcher;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -97,7 +99,7 @@
     return new ForProjectImpl();
   }
 
-  ChangeControl controlFor(Change change) throws OrmException {
+  ChangeControl controlFor(Change change) {
     return changeControlFactory.create(
         controlForRef(change.getDest()), change.getProject(), change.getId());
   }
@@ -351,7 +353,7 @@
       try {
         checkProject(cd.change());
         return super.change(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
@@ -372,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);
         }
@@ -391,7 +394,7 @@
     }
 
     @Override
-    public BooleanCondition testCond(ProjectPermission perm) {
+    public BooleanCondition testCond(CoreOrPluginProjectPermission perm) {
       return new PermissionBackendCondition.ForProject(this, perm, getUser());
     }
 
@@ -404,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 60fd15b..9a2ecdd 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
@@ -442,7 +442,7 @@
       try {
         // TODO(hiesel) Force callers to call database() and use db instead of cd.db()
         return getProjectControl().controlFor(cd.change()).asForChange(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
     }
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index ed9d2c7..c032c46 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.extensions.annotations.RootRelative;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -226,7 +227,8 @@
     return httpModule != null;
   }
 
-  Module getHttpModule() {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public Module getHttpModule() {
     return httpModule;
   }
 
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 592bed8..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;
@@ -431,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/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
index 868d0af..ce9992e 100644
--- a/java/com/google/gerrit/server/project/ChildProjects.java
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -93,8 +93,7 @@
       Project.NameKey parent)
       throws PermissionBackendException {
     List<Project.NameKey> canSee =
-        perm.filter(ProjectPermission.ACCESS, children.get(parent))
-            .stream()
+        perm.filter(ProjectPermission.ACCESS, children.get(parent)).stream()
             .sorted()
             .collect(toList());
     children.removeAll(parent); // removing all entries prevents cycles.
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index 8912e31..b33fcb5 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
@@ -43,7 +44,7 @@
 @Singleton
 public class ContributorAgreementsChecker {
 
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final ProjectCache projectCache;
   private final Metrics metrics;
 
@@ -62,7 +63,7 @@
 
   @Inject
   ContributorAgreementsChecker(
-      UrlFormatter urlFormatter, ProjectCache projectCache, Metrics metrics) {
+      DynamicItem<UrlFormatter> urlFormatter, ProjectCache projectCache, Metrics metrics) {
     this.urlFormatter = urlFormatter;
     this.projectCache = projectCache;
     this.metrics = metrics;
@@ -129,7 +130,7 @@
           .append(iUser.getAccountId())
           .append(")");
 
-      msg.append(urlFormatter.getSettingsUrl("Agreements").orElse(""));
+      msg.append(urlFormatter.get().getSettingsUrl("Agreements").orElse(""));
       throw new AuthException(msg.toString());
     }
   }
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index e841401..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
diff --git a/java/com/google/gerrit/server/project/NoSuchChangeException.java b/java/com/google/gerrit/server/project/NoSuchChangeException.java
index 7946a3a..6f65659 100644
--- a/java/com/google/gerrit/server/project/NoSuchChangeException.java
+++ b/java/com/google/gerrit/server/project/NoSuchChangeException.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 
 /** Indicates the change does not exist. */
-public class NoSuchChangeException extends OrmException {
+public class NoSuchChangeException extends StorageException {
   private static final long serialVersionUID = 1L;
 
   public NoSuchChangeException(Change.Id key) {
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index ddeeb8a..4a85554 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
@@ -248,8 +249,7 @@
   @Override
   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
     try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
-      return all()
-          .stream()
+      return all().stream()
           .map(n -> byName.getIfPresent(n.get()))
           .filter(Objects::nonNull)
           .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
@@ -329,4 +329,14 @@
       }
     }
   }
+
+  @VisibleForTesting
+  public void evictAllByName() {
+    byName.invalidateAll();
+  }
+
+  @VisibleForTesting
+  public long sizeAllByName() {
+    return byName.size();
+  }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index f051d10..3700556 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -18,6 +18,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.common.data.Permission.isPermission;
 import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
@@ -28,6 +29,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -39,9 +41,8 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubscribeSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.mail.Address;
@@ -75,7 +76,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -741,13 +741,13 @@
     accessSections = new HashMap<>();
     sectionsWithUnknownPermissions = new HashSet<>();
     for (String refName : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
+      if (AccessSection.isValidRefSectionName(refName) && isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
           for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
             n = convertLegacyPermission(n);
-            if (isPermission(n)) {
+            if (isCoreOrPluginPermission(n)) {
               as.getPermission(n, true).setExclusiveGroup(true);
             }
           }
@@ -755,7 +755,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,
@@ -784,6 +784,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);
@@ -1241,14 +1247,12 @@
 
   private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
     for (NotifyConfig nc : sort(notifySections.values())) {
-      nc.getGroups()
-          .stream()
-          .map(gr -> gr.getUUID())
+      nc.getGroups().stream()
+          .map(GroupReference::getUUID)
           .filter(Objects::nonNull)
           .forEach(keepGroups::add);
       List<String> email =
-          nc.getGroups()
-              .stream()
+          nc.getGroups().stream()
               .map(gr -> new PermissionRule(gr).asString(false))
               .sorted()
               .collect(toList());
@@ -1360,7 +1364,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);
         }
@@ -1368,7 +1372,7 @@
     }
 
     for (String name : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(name) && !accessSections.containsKey(name)) {
+      if (AccessSection.isValidRefSectionName(name) && !accessSections.containsKey(name)) {
         rc.unsetSection(ACCESS, name);
       }
     }
@@ -1481,7 +1485,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)) {
@@ -1543,6 +1547,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/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 78505ab..921d382 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.server.project;
 
 import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
@@ -29,10 +27,8 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
-import com.google.gerrit.extensions.api.projects.ThemeInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.index.project.ProjectData;
@@ -49,7 +45,6 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TransferConfig;
@@ -57,8 +52,6 @@
 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;
@@ -84,7 +77,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;
@@ -105,11 +97,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;
 
@@ -118,7 +105,6 @@
 
   @Inject
   public ProjectState(
-      SitePaths sitePaths,
       ProjectCache projectCache,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
@@ -128,7 +114,6 @@
       TransferConfig transferConfig,
       MetricMaker metricMaker,
       @Assisted ProjectConfig config) {
-    this.sitePaths = sitePaths;
     this.projectCache = projectCache;
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.isAllUsers = config.getProject().getNameKey().equals(allUsersName);
@@ -218,8 +203,7 @@
     }
 
     // If not, we check the parents.
-    return parents()
-        .stream()
+    return parents().stream()
         .map(ProjectState::getConfig)
         .map(ProjectConfig::getRulesId)
         .anyMatch(Objects::nonNull);
@@ -497,7 +481,7 @@
             continue;
           }
 
-          if (RefConfigSection.isValid(refPattern) && match(destination, refPattern)) {
+          if (AccessSection.isValidRefSectionName(refPattern) && match(destination, refPattern)) {
             r.add(l);
             break;
           }
@@ -548,24 +532,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());
   }
@@ -597,26 +563,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()) {
@@ -625,10 +571,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/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index e61c5df..32dbe1c 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -24,6 +24,7 @@
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
@@ -51,7 +52,6 @@
 import com.google.gerrit.server.query.change.RefPredicate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -94,7 +94,7 @@
   }
 
   public CheckProjectResultInfo check(Project.NameKey projectName, CheckProjectInput input)
-      throws IOException, OrmException, RestApiException {
+      throws IOException, RestApiException {
     CheckProjectResultInfo r = new CheckProjectResultInfo();
     if (input.autoCloseableChangesCheck != null) {
       r.autoCloseableChangesCheckResult =
@@ -105,7 +105,7 @@
 
   private AutoCloseableChangesCheckResult checkForAutoCloseableChanges(
       Project.NameKey projectName, AutoCloseableChangesCheckInput input)
-      throws IOException, OrmException, RestApiException {
+      throws IOException, RestApiException {
     AutoCloseableChangesCheckResult r = new AutoCloseableChangesCheckResult();
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch is required");
@@ -256,8 +256,7 @@
       List<Predicate<ChangeData>> predicates,
       boolean fix,
       Map<Change.Key, ObjectId> changeIdToMergedSha1,
-      List<ObjectId> mergedSha1s)
-      throws OrmException {
+      List<ObjectId> mergedSha1s) {
     if (predicates.isEmpty()) {
       return ImmutableList.of();
     }
@@ -273,7 +272,7 @@
                     .setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET)
                     .query(and(basePredicate, or(predicates)));
               },
-              OrmException.class::isInstance);
+              StorageException.class::isInstance);
 
       // Result for this query that we want to return to the client.
       List<ChangeInfo> autoCloseableChangesByBranch = new ArrayList<>();
@@ -296,9 +295,7 @@
 
                 // Auto-close by commit
                 for (ObjectId patchSetSha1 :
-                    autoCloseableChange
-                        .patchSets()
-                        .stream()
+                    autoCloseableChange.patchSets().stream()
                         .map(ps -> ObjectId.fromString(ps.getRevision().get()))
                         .collect(toSet())) {
                   if (mergedSha1s.contains(patchSetSha1)) {
@@ -309,15 +306,14 @@
                 }
                 return null;
               },
-              OrmException.class::isInstance);
+              StorageException.class::isInstance);
         }
       }
 
       return autoCloseableChangesByBranch;
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 76ee8c9..8119ef5 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -16,7 +16,6 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
@@ -26,7 +25,6 @@
 import java.io.IOException;
 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.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -47,14 +45,19 @@
     this.permissionBackend = permissionBackend;
   }
 
-  /** @return true if a commit is reachable from a given set of refs. */
-  public boolean fromRefs(NameKey project, Repository repo, RevCommit commit, List<Ref> 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, 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(
@@ -62,16 +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 {
-      List<Ref> refs = repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS);
-      return fromRefs(project, repo, commit, refs);
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log(
-          "Cannot verify permissions to commit object %s in repository %s", commit.name(), project);
-      return false;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/project/RefPattern.java b/java/com/google/gerrit/server/project/RefPattern.java
index 72face2..0e916fb 100644
--- a/java/com/google/gerrit/server/project/RefPattern.java
+++ b/java/com/google/gerrit/server/project/RefPattern.java
@@ -19,8 +19,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import dk.brics.automaton.RegExp;
 import java.util.concurrent.ExecutionException;
 import java.util.regex.Pattern;
@@ -78,11 +77,11 @@
   }
 
   public static void validate(String refPattern) throws InvalidNameException {
-    if (refPattern.startsWith(RefConfigSection.REGEX_PREFIX)) {
+    if (refPattern.startsWith(AccessSection.REGEX_PREFIX)) {
       if (!Repository.isValidRefName(shortestExample(refPattern))) {
         throw new InvalidNameException(refPattern);
       }
-    } else if (refPattern.equals(RefConfigSection.ALL)) {
+    } else if (refPattern.equals(AccessSection.ALL)) {
       // This is a special case we have to allow, it fails below.
     } else if (refPattern.endsWith("/*")) {
       String prefix = refPattern.substring(0, refPattern.length() - 2);
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index f7cf8b6..eeb2a65 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -66,7 +65,7 @@
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
       ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws PermissionBackendException, OrmException {
+      throws PermissionBackendException {
     if (canRemoveReviewerWithoutPermissionCheck(
         permissionBackend, cd.change(), currentUser, reviewer, value)) {
       return true;
@@ -92,7 +91,7 @@
       Account.Id reviewer,
       int value)
       throws PermissionBackendException {
-    if (change.getStatus().equals(Change.Status.MERGED)) {
+    if (change.isMerged()) {
       return false;
     }
 
diff --git a/java/com/google/gerrit/server/project/SectionMatcher.java b/java/com/google/gerrit/server/project/SectionMatcher.java
index 3095f2f..a8ebd98 100644
--- a/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -27,7 +27,7 @@
 public class SectionMatcher extends RefPatternMatcher {
   static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
-    if (AccessSection.isValid(ref)) {
+    if (AccessSection.isValidRefSectionName(ref)) {
       return new SectionMatcher(project, section, getMatcher(ref));
     }
     return null;
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 7150fae..85d91e7 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -18,12 +18,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.SubmitRule;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collection;
@@ -91,18 +91,18 @@
     try {
       change = cd.change();
       if (change == null) {
-        throw new OrmException("Change not found");
+        throw new StorageException("Change not found");
       }
 
       projectState = projectCache.get(cd.project());
       if (projectState == null) {
         throw new NoSuchProjectException(cd.project());
       }
-    } catch (OrmException | NoSuchProjectException e) {
+    } catch (StorageException | NoSuchProjectException e) {
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if (!opts.allowClosed() && 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/SuggestParentCandidates.java b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index 99833af..d3dfdcd 100644
--- a/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -42,9 +42,7 @@
   }
 
   public List<Project.NameKey> getNameKeys() throws PermissionBackendException {
-    return permissionBackend
-        .currentUser()
-        .filter(ProjectPermission.ACCESS, readableParents())
+    return permissionBackend.currentUser().filter(ProjectPermission.ACCESS, readableParents())
         .stream()
         .sorted()
         .collect(toList());
diff --git a/java/com/google/gerrit/server/project/testing/Util.java b/java/com/google/gerrit/server/project/testing/Util.java
index abfd2bd..204fa7b 100644
--- a/java/com/google/gerrit/server/project/testing/Util.java
+++ b/java/com/google/gerrit/server/project/testing/Util.java
@@ -124,16 +124,28 @@
 
   public static PermissionRule allow(
       ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
+    return allow(project, capabilityName, group, (PermissionRange) null);
+  }
+
+  public static PermissionRule allow(
+      ProjectConfig project,
+      String capabilityName,
+      AccountGroup.UUID group,
+      PermissionRange customRange) {
     PermissionRule rule = newRule(project, group);
     project
         .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
         .getPermission(capabilityName, true)
         .add(rule);
     if (GlobalCapability.hasRange(capabilityName)) {
-      PermissionRange.WithDefaults range = GlobalCapability.getRange(capabilityName);
-      if (range != null) {
-        rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+      if (customRange == null) {
+        PermissionRange.WithDefaults range = GlobalCapability.getRange(capabilityName);
+        if (range != null) {
+          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+        }
+        return rule;
       }
+      rule.setRange(customRange.getMin(), customRange.getMax());
     }
     return rule;
   }
diff --git a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
index bd7b7fe..f4ff441 100644
--- a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.IndexUtils;
-import com.google.gwtorm.server.OrmException;
 
 public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     boolean canSee = accountControl.canSee(accountState);
     if (!canSee) {
       logger.atFine().log("Filter out non-visisble account: %s", accountState);
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 732177d..cb96bc5 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
 public class AccountPredicates {
@@ -140,7 +139,7 @@
     }
 
     @Override
-    public boolean match(AccountState object) throws OrmException {
+    public boolean match(AccountState object) {
       return true;
     }
 
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index b7e8a46..70f4a2d 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -17,7 +17,8 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.exceptions.NotSignedInException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.Schema;
@@ -37,13 +38,12 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
 /** Parses a query string meant to be applied to account objects. */
-public class AccountQueryBuilder extends QueryBuilder<AccountState> {
+public class AccountQueryBuilder extends QueryBuilder<AccountState, AccountQueryBuilder> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String FIELD_ACCOUNT = "account";
@@ -108,13 +108,13 @@
 
   @Inject
   AccountQueryBuilder(Arguments args) {
-    super(mydef);
+    super(mydef, null);
     this.args = args;
   }
 
   @Operator
   public Predicate<AccountState> cansee(String change)
-      throws QueryParseException, OrmException, PermissionBackendException {
+      throws QueryParseException, PermissionBackendException {
     ChangeNotes changeNotes = args.changeFinder.findOne(change);
     if (changeNotes == null) {
       throw error(String.format("change %s not found", change));
@@ -195,7 +195,7 @@
     if (query.startsWith("cansee:")) {
       try {
         return cansee(query.substring(7));
-      } catch (OrmException | QueryParseException | PermissionBackendException e) {
+      } catch (StorageException | QueryParseException | PermissionBackendException e) {
         // Ignore, fall back to default query
       }
     }
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index 1cb2170..8bbeb24 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.server.account.AccountState;
@@ -21,7 +22,6 @@
 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;
 
 public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
   private final PermissionBackend permissionBackend;
@@ -34,7 +34,7 @@
   }
 
   @Override
-  public boolean match(AccountState accountState) throws OrmException {
+  public boolean match(AccountState accountState) {
     try {
       permissionBackend
           .absentUser(accountState.getAccount().getId())
@@ -42,7 +42,7 @@
           .check(ChangePermission.READ);
       return true;
     } catch (PermissionBackendException e) {
-      throw new OrmException("Failed to check if account can see change", e);
+      throw new StorageException("Failed to check if account can see change", e);
     } catch (AuthException e) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 6f3194e..490991a 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -51,19 +50,19 @@
     super(queryProcessor, indexes, indexConfig);
   }
 
-  public List<AccountState> byDefault(String query) throws OrmException {
+  public List<AccountState> byDefault(String query) {
     return query(AccountPredicates.defaultPredicate(schema(), true, query));
   }
 
-  public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
+  public List<AccountState> byExternalId(String scheme, String id) {
     return byExternalId(ExternalId.Key.create(scheme, id));
   }
 
-  public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
+  public List<AccountState> byExternalId(ExternalId.Key externalId) {
     return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
   }
 
-  public List<AccountState> byFullName(String fullName) throws OrmException {
+  public List<AccountState> byFullName(String fullName) {
     return query(AccountPredicates.fullName(fullName));
   }
 
@@ -72,9 +71,8 @@
    *
    * @param email preferred email by which accounts should be found
    * @return list of accounts that have a preferred email that exactly matches the given email
-   * @throws OrmException if query cannot be parsed
    */
-  public List<AccountState> byPreferredEmail(String email) throws OrmException {
+  public List<AccountState> byPreferredEmail(String email) {
     if (hasPreferredEmailExact()) {
       return query(AccountPredicates.preferredEmailExact(email));
     }
@@ -83,8 +81,7 @@
       return ImmutableList.of();
     }
 
-    return query(AccountPredicates.preferredEmail(email))
-        .stream()
+    return query(AccountPredicates.preferredEmail(email)).stream()
         .filter(a -> a.getAccount().getPreferredEmail().equals(email))
         .collect(toList());
   }
@@ -95,9 +92,8 @@
    * @param emails preferred emails by which accounts should be found
    * @return multimap of the given emails to accounts that have a preferred email that exactly
    *     matches this email
-   * @throws OrmException if query cannot be parsed
    */
-  public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
+  public Multimap<String, AccountState> byPreferredEmail(String... emails) {
     List<String> emailList = Arrays.asList(emails);
 
     if (hasPreferredEmailExact()) {
@@ -120,8 +116,7 @@
     for (int i = 0; i < emailList.size(); i++) {
       String email = emailList.get(i);
       Set<AccountState> matchingAccounts =
-          r.get(i)
-              .stream()
+          r.get(i).stream()
               .filter(a -> a.getAccount().getPreferredEmail().equals(email))
               .collect(toSet());
       accountsByEmail.putAll(email, matchingAccounts);
@@ -129,7 +124,7 @@
     return accountsByEmail;
   }
 
-  public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException {
+  public List<AccountState> byWatchedProject(Project.NameKey project) {
     return query(AccountPredicates.watchedProject(project));
   }
 
diff --git a/java/com/google/gerrit/server/query/change/AddedPredicate.java b/java/com/google/gerrit/server/query/change/AddedPredicate.java
index 099e841..1f526c5 100644
--- a/java/com/google/gerrit/server/query/change/AddedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AddedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class AddedPredicate extends IntegerRangeChangePredicate {
   public AddedPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.ADDED.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/AfterPredicate.java b/java/com/google/gerrit/server/query/change/AfterPredicate.java
index de57b3b..df5a71d 100644
--- a/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Date;
 
 public class AfterPredicate extends TimestampRangeChangePredicate {
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.change().getLastUpdatedOn().getTime() >= cut.getTime();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/AgePredicate.java b/java/com/google/gerrit/server/query/change/AgePredicate.java
index 29f1b8a..1cf2c2f 100644
--- a/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import java.sql.Timestamp;
 
 public class AgePredicate extends TimestampRangeChangePredicate {
@@ -46,7 +45,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && change.getLastUpdatedOn().getTime() <= cut;
   }
diff --git a/java/com/google/gerrit/server/query/change/AndChangeSource.java b/java/com/google/gerrit/server/query/change/AndChangeSource.java
index ff1ab23..4a3b936 100644
--- a/java/com/google/gerrit/server/query/change/AndChangeSource.java
+++ b/java/com/google/gerrit/server/query/change/AndChangeSource.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.index.query.AndSource;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import java.util.Collection;
 import java.util.List;
 
@@ -43,13 +41,9 @@
   }
 
   @Override
-  protected List<ChangeData> transformBuffer(List<ChangeData> buffer) throws OrmRuntimeException {
+  protected List<ChangeData> transformBuffer(List<ChangeData> buffer) {
     if (!hasChange()) {
-      try {
-        ChangeData.ensureChangeLoaded(buffer);
-      } catch (OrmException e) {
-        throw new OrmRuntimeException(e);
-      }
+      ChangeData.ensureChangeLoaded(buffer);
     }
     return super.transformBuffer(buffer);
   }
diff --git a/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
index 63f7467..fb19e85 100644
--- a/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class AssigneePredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     if (id.get() == ChangeField.NO_ASSIGNEE) {
       Account.Id assignee = object.change().getAssignee();
       return assignee == null;
diff --git a/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
index 3ee3352..79914a3 100644
--- a/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class AuthorPredicate extends ChangeIndexPredicate {
   public AuthorPredicate(String value) {
@@ -27,12 +25,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/BeforePredicate.java b/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 4d6ed69..dacabc0 100644
--- a/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Date;
 
 public class BeforePredicate extends TimestampRangeChangePredicate {
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.change().getLastUpdatedOn().getTime() <= cut.getTime();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/BooleanPredicate.java b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index 5930b74..68f83e8 100644
--- a/java/com/google/gerrit/server/query/change/BooleanPredicate.java
+++ b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.index.FieldDef;
-import com.google.gwtorm.server.OrmException;
 
 public class BooleanPredicate extends ChangeIndexPredicate {
   public BooleanPredicate(FieldDef<ChangeData, String> field) {
@@ -23,7 +22,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     return getValue().equals(getField().get(object));
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 787980c..619569f 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -73,7 +74,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -89,9 +89,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Stream;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -101,7 +98,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class ChangeData {
-  public static List<Change> asChanges(List<ChangeData> changeDatas) throws OrmException {
+  public static List<Change> asChanges(List<ChangeData> changeDatas) {
     List<Change> result = new ArrayList<>(changeDatas.size());
     for (ChangeData cd : changeDatas) {
       result.add(cd.change());
@@ -113,7 +110,7 @@
     return changes.stream().collect(toMap(ChangeData::getId, Function.identity()));
   }
 
-  public static void ensureChangeLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureChangeLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
@@ -124,7 +121,7 @@
     }
   }
 
-  public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
@@ -135,7 +132,7 @@
     }
   }
 
-  public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
@@ -146,8 +143,7 @@
     }
   }
 
-  public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes)
-      throws OrmException {
+  public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
@@ -158,7 +154,7 @@
     }
   }
 
-  public static void ensureMessagesLoaded(Iterable<ChangeData> changes) throws OrmException {
+  public static void ensureMessagesLoaded(Iterable<ChangeData> changes) {
     ChangeData first = Iterables.getFirst(changes, null);
     if (first == null) {
       return;
@@ -169,11 +165,10 @@
     }
   }
 
-  public static void ensureReviewedByLoadedForOpenChanges(Iterable<ChangeData> changes)
-      throws OrmException {
+  public static void ensureReviewedByLoadedForOpenChanges(Iterable<ChangeData> changes) {
     List<ChangeData> pending = new ArrayList<>();
     for (ChangeData cd : changes) {
-      if (cd.reviewedBy == null && cd.change().getStatus().isOpen()) {
+      if (cd.reviewedBy == null && cd.change().isNew()) {
         pending.add(cd);
       }
     }
@@ -362,14 +357,14 @@
     return allUsersName;
   }
 
-  public void setCurrentFilePaths(List<String> filePaths) throws OrmException {
+  public void setCurrentFilePaths(List<String> filePaths) {
     PatchSet ps = currentPatchSet();
     if (ps != null) {
       currentFiles = ImmutableList.copyOf(filePaths);
     }
   }
 
-  public List<String> currentFilePaths() throws IOException, OrmException {
+  public List<String> currentFilePaths() {
     if (currentFiles == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -380,7 +375,7 @@
     return currentFiles;
   }
 
-  private Optional<DiffSummary> getDiffSummary() throws OrmException, IOException {
+  private Optional<DiffSummary> getDiffSummary() {
     if (diffSummary == null) {
       if (!lazyLoad) {
         return Optional.empty();
@@ -408,7 +403,7 @@
     return diffSummary;
   }
 
-  private Optional<ChangedLines> computeChangedLines() throws OrmException, IOException {
+  private Optional<ChangedLines> computeChangedLines() {
     Optional<DiffSummary> ds = getDiffSummary();
     if (ds.isPresent()) {
       return Optional.of(ds.get().getChangedLines());
@@ -416,7 +411,7 @@
     return Optional.empty();
   }
 
-  public Optional<ChangedLines> changedLines() throws OrmException, IOException {
+  public Optional<ChangedLines> changedLines() {
     if (changedLines == null) {
       if (!lazyLoad) {
         return Optional.empty();
@@ -450,7 +445,7 @@
     visibleTo = user;
   }
 
-  public Change change() throws OrmException {
+  public Change change() {
     if (change == null && lazyLoad) {
       reloadChange();
     }
@@ -461,41 +456,41 @@
     change = c;
   }
 
-  public Change reloadChange() throws OrmException {
+  public Change reloadChange() {
     try {
       notes = notesFactory.createChecked(project, legacyId);
     } catch (NoSuchChangeException e) {
-      throw new OrmException("Unable to load change " + legacyId, e);
+      throw new StorageException("Unable to load change " + legacyId, e);
     }
     change = notes.getChange();
     setPatchSets(null);
     return change;
   }
 
-  public LabelTypes getLabelTypes() throws OrmException {
+  public LabelTypes getLabelTypes() {
     if (labelTypes == null) {
       ProjectState state;
       try {
         state = projectCache.checkedGet(project());
       } catch (IOException e) {
-        throw new OrmException("project state not available", e);
+        throw new StorageException("project state not available", e);
       }
       labelTypes = state.getLabelTypes(change().getDest());
     }
     return labelTypes;
   }
 
-  public ChangeNotes notes() throws OrmException {
+  public ChangeNotes notes() {
     if (notes == null) {
       if (!lazyLoad) {
-        throw new OrmException("ChangeNotes not available, lazyLoad = false");
+        throw new StorageException("ChangeNotes not available, lazyLoad = false");
       }
       notes = notesFactory.create(project(), legacyId);
     }
     return notes;
   }
 
-  public PatchSet currentPatchSet() throws OrmException {
+  public PatchSet currentPatchSet() {
     if (currentPatchSet == null) {
       Change c = change();
       if (c == null) {
@@ -511,7 +506,7 @@
     return currentPatchSet;
   }
 
-  public List<PatchSetApproval> currentApprovals() throws OrmException {
+  public List<PatchSetApproval> currentApprovals() {
     if (currentApprovals == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -524,7 +519,7 @@
           currentApprovals =
               ImmutableList.copyOf(
                   approvalsUtil.byPatchSet(notes(), c.currentPatchSetId(), null, null));
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           if (e.getCause() instanceof NoSuchChangeException) {
             currentApprovals = Collections.emptyList();
           } else {
@@ -540,7 +535,7 @@
     currentApprovals = approvals;
   }
 
-  public String commitMessage() throws IOException, OrmException {
+  public String commitMessage() {
     if (commitMessage == null) {
       if (!loadCommitData()) {
         return null;
@@ -549,7 +544,7 @@
     return commitMessage;
   }
 
-  public List<FooterLine> commitFooters() throws IOException, OrmException {
+  public List<FooterLine> commitFooters() {
     if (commitFooters == null) {
       if (!loadCommitData()) {
         return null;
@@ -558,11 +553,11 @@
     return commitFooters;
   }
 
-  public ListMultimap<String, String> trackingFooters() throws IOException, OrmException {
+  public ListMultimap<String, String> trackingFooters() {
     return trackingFooters.extract(commitFooters());
   }
 
-  public PersonIdent getAuthor() throws IOException, OrmException {
+  public PersonIdent getAuthor() {
     if (author == null) {
       if (!loadCommitData()) {
         return null;
@@ -571,7 +566,7 @@
     return author;
   }
 
-  public PersonIdent getCommitter() throws IOException, OrmException {
+  public PersonIdent getCommitter() {
     if (committer == null) {
       if (!loadCommitData()) {
         return null;
@@ -580,9 +575,7 @@
     return committer;
   }
 
-  private boolean loadCommitData()
-      throws OrmException, RepositoryNotFoundException, IOException, MissingObjectException,
-          IncorrectObjectTypeException {
+  private boolean loadCommitData() {
     PatchSet ps = currentPatchSet();
     if (ps == null) {
       return false;
@@ -596,15 +589,14 @@
       author = c.getAuthorIdent();
       committer = c.getCommitterIdent();
       parentCount = c.getParentCount();
+    } catch (IOException e) {
+      throw new StorageException(e);
     }
     return true;
   }
 
-  /**
-   * @return patches for the change, in patch set ID order.
-   * @throws OrmException an error occurred reading the database.
-   */
-  public Collection<PatchSet> patchSets() throws OrmException {
+  /** @return patches for the change, in patch set ID order. */
+  public Collection<PatchSet> patchSets() {
     if (patchSets == null) {
       patchSets = psUtil.byChange(notes());
     }
@@ -616,11 +608,8 @@
     this.patchSets = patchSets;
   }
 
-  /**
-   * @return patch with the given ID, or null if it does not exist.
-   * @throws OrmException an error occurred reading the database.
-   */
-  public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
+  /** @return patch with the given ID, or null if it does not exist. */
+  public PatchSet patchSet(PatchSet.Id psId) {
     if (currentPatchSet != null && currentPatchSet.getId().equals(psId)) {
       return currentPatchSet;
     }
@@ -635,9 +624,8 @@
   /**
    * @return all patch set approvals for the change, keyed by ID, ordered by timestamp within each
    *     patch set.
-   * @throws OrmException an error occurred reading the database.
    */
-  public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() throws OrmException {
+  public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() {
     if (allApprovals == null) {
       if (!lazyLoad) {
         return ImmutableListMultimap.of();
@@ -647,15 +635,12 @@
     return allApprovals;
   }
 
-  /**
-   * @return The submit ('SUBM') approval label
-   * @throws OrmException an error occurred reading the database.
-   */
-  public Optional<PatchSetApproval> getSubmitApproval() throws OrmException {
+  /** @return The submit ('SUBM') approval label */
+  public Optional<PatchSetApproval> getSubmitApproval() {
     return currentApprovals().stream().filter(PatchSetApproval::isLegacySubmit).findFirst();
   }
 
-  public ReviewerSet reviewers() throws OrmException {
+  public ReviewerSet reviewers() {
     if (reviewers == null) {
       if (!lazyLoad) {
         return ReviewerSet.empty();
@@ -673,7 +658,7 @@
     return reviewers;
   }
 
-  public ReviewerByEmailSet reviewersByEmail() throws OrmException {
+  public ReviewerByEmailSet reviewersByEmail() {
     if (reviewersByEmail == null) {
       if (!lazyLoad) {
         return ReviewerByEmailSet.empty();
@@ -699,7 +684,7 @@
     return this.pendingReviewers;
   }
 
-  public ReviewerSet pendingReviewers() throws OrmException {
+  public ReviewerSet pendingReviewers() {
     if (pendingReviewers == null) {
       if (!lazyLoad) {
         return ReviewerSet.empty();
@@ -717,7 +702,7 @@
     return pendingReviewersByEmail;
   }
 
-  public ReviewerByEmailSet pendingReviewersByEmail() throws OrmException {
+  public ReviewerByEmailSet pendingReviewersByEmail() {
     if (pendingReviewersByEmail == null) {
       if (!lazyLoad) {
         return ReviewerByEmailSet.empty();
@@ -727,7 +712,7 @@
     return pendingReviewersByEmail;
   }
 
-  public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException {
+  public List<ReviewerStatusUpdate> reviewerUpdates() {
     if (reviewerUpdates == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -745,7 +730,7 @@
     return reviewerUpdates;
   }
 
-  public Collection<Comment> publishedComments() throws OrmException {
+  public Collection<Comment> publishedComments() {
     if (publishedComments == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -755,7 +740,7 @@
     return publishedComments;
   }
 
-  public Collection<RobotComment> robotComments() throws OrmException {
+  public Collection<RobotComment> robotComments() {
     if (robotComments == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -765,7 +750,7 @@
     return robotComments;
   }
 
-  public Integer unresolvedCommentCount() throws OrmException {
+  public Integer unresolvedCommentCount() {
     if (unresolvedCommentCount == null) {
       if (!lazyLoad) {
         return null;
@@ -819,7 +804,7 @@
     this.unresolvedCommentCount = count;
   }
 
-  public Integer totalCommentCount() throws OrmException {
+  public Integer totalCommentCount() {
     if (totalCommentCount == null) {
       if (!lazyLoad) {
         return null;
@@ -836,7 +821,7 @@
     this.totalCommentCount = count;
   }
 
-  public List<ChangeMessage> messages() throws OrmException {
+  public List<ChangeMessage> messages() {
     if (messages == null) {
       if (!lazyLoad) {
         return Collections.emptyList();
@@ -880,15 +865,15 @@
   }
 
   @Nullable
-  public Boolean isMergeable() throws OrmException {
+  public Boolean isMergeable() {
     if (mergeable == null) {
       Change c = change();
       if (c == null) {
         return null;
       }
-      if (c.getStatus() == Change.Status.MERGED) {
+      if (c.isMerged()) {
         mergeable = true;
-      } else if (c.getStatus() == Change.Status.ABANDONED) {
+      } else if (c.isAbandoned()) {
         return null;
       } else if (c.isWorkInProgress()) {
         return null;
@@ -920,18 +905,18 @@
                   c.getDest(),
                   repo);
         } catch (IOException e) {
-          throw new OrmException(e);
+          throw new StorageException(e);
         }
       }
     }
     return mergeable;
   }
 
-  public Set<Account.Id> editsByUser() throws OrmException {
+  public Set<Account.Id> editsByUser() {
     return editRefs().keySet();
   }
 
-  public Map<Account.Id, Ref> editRefs() throws OrmException {
+  public Map<Account.Id, Ref> editRefs() {
     if (editsByUser == null) {
       if (!lazyLoad) {
         return Collections.emptyMap();
@@ -953,17 +938,17 @@
           }
         }
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
     return editsByUser;
   }
 
-  public Set<Account.Id> draftsByUser() throws OrmException {
+  public Set<Account.Id> draftsByUser() {
     return draftRefs().keySet();
   }
 
-  public Map<Account.Id, Ref> draftRefs() throws OrmException {
+  public Map<Account.Id, Ref> draftRefs() {
     if (draftsByUser == null) {
       if (!lazyLoad) {
         return Collections.emptyMap();
@@ -991,7 +976,7 @@
     return draftsByUser;
   }
 
-  public boolean isReviewedBy(Account.Id accountId) throws OrmException {
+  public boolean isReviewedBy(Account.Id accountId) {
     Collection<String> stars = stars(accountId);
 
     PatchSet ps = currentPatchSet();
@@ -1008,7 +993,7 @@
     return reviewedBy().contains(accountId);
   }
 
-  public Set<Account.Id> reviewedBy() throws OrmException {
+  public Set<Account.Id> reviewedBy() {
     if (reviewedBy == null) {
       if (!lazyLoad) {
         return Collections.emptySet();
@@ -1040,7 +1025,7 @@
     this.reviewedBy = reviewedBy;
   }
 
-  public Set<String> hashtags() throws OrmException {
+  public Set<String> hashtags() {
     if (hashtags == null) {
       if (!lazyLoad) {
         return Collections.emptySet();
@@ -1054,7 +1039,7 @@
     this.hashtags = hashtags;
   }
 
-  public ImmutableListMultimap<Account.Id, String> stars() throws OrmException {
+  public ImmutableListMultimap<Account.Id, String> stars() {
     if (stars == null) {
       if (!lazyLoad) {
         return ImmutableListMultimap.of();
@@ -1072,7 +1057,7 @@
     this.stars = ImmutableListMultimap.copyOf(stars);
   }
 
-  public ImmutableMap<Account.Id, StarRef> starRefs() throws OrmException {
+  public ImmutableMap<Account.Id, StarRef> starRefs() {
     if (starRefs == null) {
       if (!lazyLoad) {
         return ImmutableMap.of();
@@ -1082,7 +1067,7 @@
     return starRefs;
   }
 
-  public Set<String> stars(Account.Id accountId) throws OrmException {
+  public Set<String> stars(Account.Id accountId) {
     if (starsOf != null) {
       if (!starsOf.accountId().equals(accountId)) {
         starsOf = null;
@@ -1106,14 +1091,14 @@
    *     false otherwise.
    */
   @Nullable
-  public Boolean isPureRevert() throws OrmException {
+  public Boolean isPureRevert() {
     if (change().getRevertOf() == null) {
       return null;
     }
     try {
-      return pureRevert.get(notes(), null).isPureRevert;
+      return pureRevert.get(notes(), Optional.empty());
     } catch (IOException | BadRequestException | ResourceConflictException e) {
-      throw new OrmException("could not compute pure revert", e);
+      throw new StorageException("could not compute pure revert", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index d541d18..74ad0ef 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 /** Predicate over Change-Id strings (aka Change.Key). */
 public class ChangeIdPredicate extends ChangeIndexPredicate {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Change change = cd.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 1eb2770..7428e3a 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -17,9 +17,22 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.Predicate;
 
 public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
     implements Matchable<ChangeData> {
+  /**
+   * Returns an index predicate that matches no changes in the index.
+   *
+   * <p>This predicate should be used in preference to a non-index predicate (such as {@code
+   * Predicate.not(Predicate.any())}), since it can be matched efficiently against the index.
+   *
+   * @return an index predicate matching no changes.
+   */
+  public static Predicate<ChangeData> none() {
+    return ChangeStatusPredicate.NONE;
+  }
+
   protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String value) {
     super(def, value);
   }
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index a793cf2..60b4d38 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -58,7 +58,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
@@ -80,7 +80,7 @@
         return false;
       }
     } catch (IOException e) {
-      throw new OrmException("unable to read project state", e);
+      throw new StorageException("unable to read project state", e);
     }
 
     PermissionBackend.WithUser withUser =
@@ -97,7 +97,7 @@
             cd, cd.project());
         return false;
       }
-      throw new OrmException("unable to check permissions on change " + cd.getId(), e);
+      throw new StorageException("unable to check permissions on change " + cd.getId(), e);
     } catch (AuthException e) {
       logger.atFine().log("Filter out non-visisble change: %s", cd);
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index f5df87b..0227153 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;
@@ -29,9 +31,9 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.exceptions.NotSignedInException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.SchemaUtil;
@@ -53,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;
@@ -73,7 +76,6 @@
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.submit.SubmitDryRun;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -95,7 +97,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Parses a query string meant to be applied to change objects. */
-public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
+public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> {
   public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {}
 
   /**
@@ -121,8 +123,8 @@
 
   static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
 
-  // NOTE: As new search operations are added, please keep the
-  // SearchSuggestOracle up to date.
+  // NOTE: As new search operations are added, please keep the suggestions in
+  // gr-search-bar.js up to date.
 
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AGE = "age";
@@ -136,7 +138,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";
@@ -401,25 +407,15 @@
 
   @Inject
   ChangeQueryBuilder(Arguments args) {
-    super(mydef);
-    this.args = args;
-    setupDynamicOperators();
+    this(mydef, args);
   }
 
   @VisibleForTesting
-  protected ChangeQueryBuilder(
-      Definition<ChangeData, ? extends QueryBuilder<ChangeData>> def, Arguments args) {
-    super(def);
+  protected ChangeQueryBuilder(Definition<ChangeData, ChangeQueryBuilder> def, Arguments args) {
+    super(def, args.opFactories);
     this.args = args;
   }
 
-  private void setupDynamicOperators() {
-    for (Extension<ChangeOperatorFactory> e : args.opFactories) {
-      String name = e.getExportName() + "_" + e.getPluginName();
-      opFactories.put(name, e.getProvider().get());
-    }
-  }
-
   public Arguments getArgs() {
     return args;
   }
@@ -612,7 +608,7 @@
   }
 
   @Operator
-  public Predicate<ChangeData> conflicts(String value) throws OrmException, QueryParseException {
+  public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
     List<Change> changes = parseChange(value);
     List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
     for (Change c : changes) {
@@ -733,16 +729,68 @@
   }
 
   @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 {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = null;
     AccountGroup.UUID group = null;
 
     // Parse for:
-    // label:CodeReview=1,user=jsmith or
-    // label:CodeReview=1,jsmith or
-    // label:CodeReview=1,group=android_approvers or
-    // label:CodeReview=1,android_approvers
+    // label:Code-Review=1,user=jsmith or
+    // label:Code-Review=1,jsmith or
+    // label:Code-Review=1,group=android_approvers or
+    // label:Code-Review=1,android_approvers
     // user/groups without a label will first attempt to match user
     // Special case: votes by owners can be tracked with ",owner":
     // label:Code-Review+2,owner
@@ -834,7 +882,7 @@
 
   @Operator
   public Predicate<ChangeData> starredby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return starredby(parseAccount(who));
   }
 
@@ -852,7 +900,7 @@
 
   @Operator
   public Predicate<ChangeData> watchedby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
 
@@ -877,7 +925,7 @@
 
   @Operator
   public Predicate<ChangeData> draftby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
     for (Account.Id id : m) {
@@ -890,26 +938,24 @@
     return new HasDraftByPredicate(who);
   }
 
-  private boolean isSelf(String who) {
-    return "self".equals(who) || "me".equals(who);
-  }
-
   @Operator
   public Predicate<ChangeData> visibleto(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     if (isSelf(who)) {
       return is_visible();
     }
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (!m.isEmpty()) {
-      List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
-      for (Account.Id id : m) {
-        return visibleto(args.userFactory.create(id));
+    try {
+      return Predicate.or(
+          parseAccount(who).stream()
+              .map(a -> visibleto(args.userFactory.create(a)))
+              .collect(toImmutableList()));
+    } catch (QueryParseException e) {
+      if (e instanceof QueryRequiresAuthException) {
+        throw e;
       }
-      return Predicate.or(p);
+      // Otherwise continue: if it's not an account, maybe it's a group?
     }
 
-    // If its not an account, maybe its a group?
     Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<>();
@@ -937,13 +983,13 @@
 
   @Operator
   public Predicate<ChangeData> o(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return owner(who);
   }
 
   @Operator
   public Predicate<ChangeData> owner(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return owner(parseAccount(who));
   }
 
@@ -956,7 +1002,7 @@
   }
 
   private Predicate<ChangeData> ownerDefaultField(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = parseAccount(who);
     if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
       return Predicate.any();
@@ -966,7 +1012,7 @@
 
   @Operator
   public Predicate<ChangeData> assignee(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return assignee(parseAccount(who));
   }
 
@@ -1001,23 +1047,23 @@
 
   @Operator
   public Predicate<ChangeData> r(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who);
   }
 
   @Operator
   public Predicate<ChangeData> reviewer(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who, false);
   }
 
   private Predicate<ChangeData> reviewerDefaultField(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewer(who, true);
   }
 
   private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Predicate<ChangeData> byState =
         reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
     if (Objects.equals(byState, Predicate.<ChangeData>any())) {
@@ -1031,7 +1077,7 @@
 
   @Operator
   public Predicate<ChangeData> cc(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return reviewerByState(who, ReviewerStateInternal.CC, false);
   }
 
@@ -1085,7 +1131,7 @@
 
   @Operator
   public Predicate<ChangeData> commentby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return commentby(parseAccount(who));
   }
 
@@ -1099,7 +1145,7 @@
 
   @Operator
   public Predicate<ChangeData> from(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Set<Account.Id> ownerIds = parseAccount(who);
     return Predicate.or(owner(ownerIds), commentby(ownerIds));
   }
@@ -1124,7 +1170,7 @@
 
   @Operator
   public Predicate<ChangeData> reviewedby(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     return IsReviewedPredicate.create(parseAccount(who));
   }
 
@@ -1214,7 +1260,7 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     try {
@@ -1222,13 +1268,13 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(file(query));
     try {
       predicates.add(label(query));
-    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
+    } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(commit(query));
@@ -1282,15 +1328,15 @@
   }
 
   private Set<Account.Id> parseAccount(String who)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    if (isSelf(who)) {
-      return Collections.singleton(self());
+      throws QueryParseException, IOException, ConfigInvalidException {
+    try {
+      return args.accountResolver.resolve(who).asNonEmptyIdSet();
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new QueryRequiresAuthException(e.getMessage(), e);
+      }
+      throw new QueryParseException(e.getMessage(), e);
     }
-    Set<Account.Id> matches = args.accountResolver.findAll(who);
-    if (matches.isEmpty()) {
-      throw error("User " + who + " not found");
-    }
-    return matches;
   }
 
   private GroupReference parseGroup(String group) throws QueryParseException {
@@ -1301,7 +1347,7 @@
     return g;
   }
 
-  private List<Change> parseChange(String value) throws OrmException, QueryParseException {
+  private List<Change> parseChange(String value) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(value).matches()) {
       return asChanges(args.queryProvider.get().byLegacyChangeId(Change.Id.parse(value)));
     } else if (PAT_CHANGE_ID.matcher(value).matches()) {
@@ -1328,7 +1374,7 @@
 
   public Predicate<ChangeData> reviewerByState(
       String who, ReviewerStateInternal state, boolean forDefaultField)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+      throws QueryParseException, IOException, ConfigInvalidException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
     if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
       Address address = Address.tryParse(who);
@@ -1343,8 +1389,7 @@
       if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
         reviewerPredicate =
             Predicate.or(
-                accounts
-                    .stream()
+                accounts.stream()
                     .map(id -> ReviewerPredicate.forState(id, state))
                     .collect(toList()));
       }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 18aab18..f9263a9 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,10 +17,11 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimap;
+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;
@@ -32,6 +33,9 @@
 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;
@@ -41,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;
 
@@ -54,22 +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);
-  }
-
+    implements DynamicOptions.BeanReceiver, DynamicOptions.BeanProvider {
   private final Provider<CurrentUser> userProvider;
   private final ChangeNotes.Factory notesFactory;
-  private final DynamicMap<ChangeAttributeFactory> attributeFactories;
-  private final Multimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin =
-      HashMultimap.create();
+  private final ImmutableListMultimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousUserProvider;
@@ -91,7 +81,7 @@
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
       ChangeNotes.Factory notesFactory,
-      DynamicMap<ChangeAttributeFactory> attributeFactories,
+      DynamicSet<ChangeAttributeFactory> attributeFactories,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       Provider<AnonymousUser> anonymousUserProvider) {
@@ -105,11 +95,16 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.notesFactory = notesFactory;
-    this.attributeFactories = attributeFactories;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.anonymousUserProvider = anonymousUserProvider;
-    setupAttributeFactories();
+
+    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
@@ -129,39 +124,21 @@
     dynamicBeans.put(plugin, dynamicBean);
   }
 
+  @Override
   public DynamicBean getDynamicBean(String plugin) {
     return dynamicBeans.get(plugin);
   }
 
-  public void setupAttributeFactories() {
-    for (String plugin : attributeFactories.plugins()) {
-      for (Provider<ChangeAttributeFactory> provider :
-          attributeFactories.byPlugin(plugin).values()) {
-        attributeFactoriesByPlugin.put(plugin, provider.get());
-      }
-    }
+  public PluginDefinedAttributesFactory getAttributesFactory() {
+    return this::buildPluginInfo;
   }
 
-  @Override
-  public List<PluginDefinedInfo> create(ChangeData cd) {
-    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size());
-    for (Map.Entry<String, ChangeAttributeFactory> e : attributeFactoriesByPlugin.entries()) {
-      String plugin = e.getKey();
-      PluginDefinedInfo pda = null;
-      try {
-        pda = e.getValue().create(cd, this, plugin);
-      } catch (RuntimeException ex) {
-        /* 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;
+  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
+    return PluginDefinedAttributesFactories.createAll(
+        cd,
+        this,
+        attributeFactoriesByPlugin.entries().stream()
+            .map(e -> new Extension<>(e.getKey(), e::getValue)));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 8dc17d3..66790e7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -41,7 +40,7 @@
  */
 public final class ChangeStatusPredicate extends ChangeIndexPredicate {
   private static final String INVALID_STATUS = "__invalid__";
-  private static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
+  static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
 
   private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
   private static final Predicate<ChangeData> CLOSED;
@@ -119,7 +118,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && Objects.equals(status, change.getStatus());
   }
diff --git a/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
index 7ad7afe..0747bb2 100644
--- a/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Objects;
 
 public class CommentByPredicate extends ChangeIndexPredicate {
@@ -34,7 +33,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     for (ChangeMessage m : cd.messages()) {
       if (Objects.equals(m.getAuthor(), id)) {
         return true;
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 5a6d186..d193bb6 100644
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gwtorm.server.OrmException;
 
 public class CommentPredicate extends ChangeIndexPredicate {
   protected final ChangeIndex index;
@@ -30,7 +30,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     try {
       Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -39,7 +39,7 @@
         }
       }
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
 
     return false;
diff --git a/java/com/google/gerrit/server/query/change/CommitPredicate.java b/java/com/google/gerrit/server/query/change/CommitPredicate.java
index d1ae529..567f58d 100644
--- a/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -20,7 +20,6 @@
 
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.OrmException;
 
 public class CommitPredicate extends ChangeIndexPredicate {
   static FieldDef<ChangeData, ?> commitField(String id) {
@@ -35,7 +34,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     String id = getValue().toLowerCase();
     for (PatchSet p : object.patchSets()) {
       if (equals(p, id)) {
diff --git a/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
index 797cb9d..1dcf97f 100644
--- a/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class CommitterPredicate extends ChangeIndexPredicate {
   public CommitterPredicate(String value) {
@@ -27,12 +25,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 7a399f2..d415f71 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -14,13 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.flogger.LazyArgs.lazy;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -29,7 +36,6 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gerrit.server.submit.IntegrationException;
 import com.google.gerrit.server.submit.SubmitDryRun;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -41,20 +47,27 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class ConflictsPredicate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   // UI code may depend on this string, so use caution when changing.
   protected static final String TOO_MANY_FILES = "too many files to find conflicts";
 
   private ConflictsPredicate() {}
 
   public static Predicate<ChangeData> create(Arguments args, String value, Change c)
-      throws QueryParseException, OrmException {
+      throws QueryParseException {
     ChangeData cd;
     List<String> files;
     try {
       cd = args.changeDataFactory.create(c);
       files = cd.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
+    } catch (StorageException e) {
+      warnWithOccasionalStackTrace(
+          e,
+          "Error constructing conflicts predicates for change %s in %s",
+          c.getId(),
+          c.getProject());
+      return ChangeIndexPredicate.none();
     }
 
     if (3 + files.size() > args.indexConfig.maxTerms()) {
@@ -95,51 +108,66 @@
     }
 
     @Override
-    public boolean match(ChangeData object) throws OrmException {
-      Change otherChange = object.change();
-      if (otherChange == null || !otherChange.getDest().equals(dest)) {
-        return false;
-      }
-
-      SubmitTypeRecord str = object.submitTypeRecord();
-      if (!str.isOk()) {
-        return false;
-      }
-
-      ProjectState projectState;
+    public boolean match(ChangeData object) {
+      Change.Id id = object.getId();
+      Project.NameKey otherProject = null;
+      ObjectId other = null;
       try {
-        projectState = changeDataCache.getProjectState();
-      } catch (NoSuchProjectException e) {
-        return false;
-      }
+        Change otherChange = object.change();
+        if (otherChange == null || !otherChange.getDest().equals(dest)) {
+          return false;
+        }
+        otherProject = otherChange.getProject();
 
-      ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
-      ConflictKey conflictsKey =
-          ConflictKey.create(
-              changeDataCache.getTestAgainst(),
-              other,
-              str.type,
-              projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
-      Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
-      if (maybeConflicts != null) {
-        return maybeConflicts;
-      }
+        SubmitTypeRecord str = object.submitTypeRecord();
+        if (!str.isOk()) {
+          return false;
+        }
 
-      try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
-          CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        boolean conflicts =
-            !args.submitDryRun.run(
-                str.type,
-                repo,
-                rw,
-                otherChange.getDest(),
+        ProjectState projectState;
+        try {
+          projectState = changeDataCache.getProjectState();
+        } catch (NoSuchProjectException e) {
+          return false;
+        }
+
+        other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
+        ConflictKey conflictsKey =
+            ConflictKey.create(
                 changeDataCache.getTestAgainst(),
                 other,
-                getAlreadyAccepted(repo, rw));
-        args.conflictsCache.put(conflictsKey, conflicts);
-        return conflicts;
-      } catch (IntegrationException | NoSuchProjectException | IOException e) {
-        throw new OrmException(e);
+                str.type,
+                projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
+        Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
+        if (maybeConflicts != null) {
+          return maybeConflicts;
+        }
+
+        try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
+            CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+          boolean conflicts =
+              !args.submitDryRun.run(
+                  null,
+                  str.type,
+                  repo,
+                  rw,
+                  otherChange.getDest(),
+                  changeDataCache.getTestAgainst(),
+                  other,
+                  getAlreadyAccepted(repo, rw));
+          args.conflictsCache.put(conflictsKey, conflicts);
+          return conflicts;
+        }
+      } catch (IntegrationException | NoSuchProjectException | StorageException | IOException e) {
+        ObjectId finalOther = other;
+        warnWithOccasionalStackTrace(
+            e,
+            "Merge failure checking conflicts of change %s in %s (%s): %s",
+            id,
+            firstNonNull(otherProject, "unknown project"),
+            lazy(() -> finalOther != null ? finalOther.name() : "unknown commit"),
+            e.getMessage());
+        return false;
       }
     }
 
@@ -158,7 +186,7 @@
           accepted.add(rw.parseCommit(tip));
         }
         return accepted;
-      } catch (OrmException | IOException e) {
+      } catch (StorageException | IOException e) {
         throw new IntegrationException("Failed to determine already accepted commits.", e);
       }
     }
@@ -177,7 +205,7 @@
       this.projectCache = projectCache;
     }
 
-    ObjectId getTestAgainst() throws OrmException {
+    ObjectId getTestAgainst() {
       if (testAgainst == null) {
         testAgainst = ObjectId.fromString(cd.currentPatchSet().getRevision().get());
       }
@@ -201,4 +229,13 @@
       return alreadyAccepted;
     }
   }
+
+  private static void warnWithOccasionalStackTrace(Throwable cause, String format, Object... args) {
+    logger.atWarning().logVarargs(format, args);
+    logger
+        .atWarning()
+        .withCause(cause)
+        .atMostEvery(1, MINUTES)
+        .logVarargs("(Re-logging with stack trace) " + format, args);
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/DeletedPredicate.java b/java/com/google/gerrit/server/query/change/DeletedPredicate.java
index 6232fc5..d4bdc67 100644
--- a/java/com/google/gerrit/server/query/change/DeletedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DeletedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class DeletedPredicate extends IntegerRangeChangePredicate {
   public DeletedPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.DELETED.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/DeltaPredicate.java b/java/com/google/gerrit/server/query/change/DeltaPredicate.java
index aae0a20..821ec94 100644
--- a/java/com/google/gerrit/server/query/change/DeltaPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DeltaPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class DeltaPredicate extends IntegerRangeChangePredicate {
   public DeltaPredicate(String value) throws QueryParseException {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.DELTA.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index a824a87..702745e 100644
--- a/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 import java.util.Set;
 
 public class DestinationPredicate extends PostFilterPredicate<ChangeData> {
@@ -29,7 +28,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
new file mode 100644
index 0000000..3ab3e26
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class DirectoryPredicate extends ChangeIndexPredicate {
+  private static String clean(String directory) {
+    return CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US);
+  }
+
+  DirectoryPredicate(String value) {
+    super(ChangeField.DIRECTORY, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getDirectories(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/EditByPredicate.java b/java/com/google/gerrit/server/query/change/EditByPredicate.java
index 3238dc9..dfe7310 100644
--- a/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class EditByPredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.editsByUser().contains(id);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index b5a2d05..9c033b6 100644
--- a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.OrmException;
 
 public class EqualsFilePredicate extends ChangeIndexPredicate {
   public static Predicate<ChangeData> create(Arguments args, String value) {
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     return ChangeField.getFileParts(object).contains(value);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 430d8c3..acf2e25 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 
 public class EqualsLabelPredicate extends ChangeIndexPredicate {
@@ -53,7 +52,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change c = object.change();
     if (c == null) {
       // The change has disappeared.
diff --git a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
index fc00283..76936fa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -15,10 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Collections;
-import java.util.List;
 
 public class EqualsPathPredicate extends ChangeIndexPredicate {
   public EqualsPathPredicate(String fieldName, String value) {
@@ -26,14 +23,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    List<String> files;
-    try {
-      files = object.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return Collections.binarySearch(files, value) >= 0;
+  public boolean match(ChangeData object) {
+    return Collections.binarySearch(object.currentFilePaths(), value) >= 0;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
index bca5d3b..c1b6928 100644
--- a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Locale;
 
 public class ExactAuthorPredicate extends ChangeIndexPredicate {
@@ -28,12 +26,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
index 3fae5e5..dac63af 100644
--- a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -18,8 +18,6 @@
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 import java.util.Locale;
 
 public class ExactCommitterPredicate extends ChangeIndexPredicate {
@@ -28,12 +26,8 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    try {
-      return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
+  public boolean match(ChangeData object) {
+    return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index 138cce5..c6ade75e 100644
--- a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 
 public class ExactTopicPredicate extends ChangeIndexPredicate {
   public ExactTopicPredicate(String topic) {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
new file mode 100644
index 0000000..bddd2ec
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class FileExtensionListPredicate extends ChangeIndexPredicate {
+  private static String clean(String extList) {
+    return Splitter.on(',').splitToList(extList).stream()
+        .map(FileExtensionPredicate::clean)
+        .distinct()
+        .sorted()
+        .collect(joining(","));
+  }
+
+  FileExtensionListPredicate(String value) {
+    super(ChangeField.ONLY_EXTENSIONS, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getAllExtensionsAsList(cd).equals(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
new file mode 100644
index 0000000..ee573a7
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class FileExtensionPredicate extends ChangeIndexPredicate {
+  static String clean(String ext) {
+    if (ext.startsWith(".")) {
+      ext = ext.substring(1);
+    }
+    return ext.toLowerCase(Locale.US);
+  }
+
+  FileExtensionPredicate(String value) {
+    super(ChangeField.EXTENSION, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData object) {
+    return ChangeField.getExtensions(object).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
new file mode 100644
index 0000000..4d7588c
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
+
+public class FooterPredicate extends ChangeIndexPredicate {
+  private static String clean(String value) {
+    int indexEquals = value.indexOf('=');
+    int indexColon = value.indexOf(':');
+
+    // footer key cannot contain '='
+    if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
+      value = value.substring(0, indexEquals) + ": " + value.substring(indexEquals + 1);
+    }
+    return value.toLowerCase(Locale.US);
+  }
+
+  FooterPredicate(String value) {
+    super(ChangeField.FOOTER, clean(value));
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getFooters(cd).contains(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 545b668..140f26b 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -17,12 +17,12 @@
 import static com.google.gerrit.server.index.change.ChangeField.FUZZY_TOPIC;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gwtorm.server.OrmException;
 
 public class FuzzyTopicPredicate extends ChangeIndexPredicate {
   protected final ChangeIndex index;
@@ -33,7 +33,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Change change = cd.change();
     if (change == null) {
       return false;
@@ -48,7 +48,7 @@
           index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
       return !Iterables.isEmpty(results);
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/GroupPredicate.java b/java/com/google/gerrit/server/query/change/GroupPredicate.java
index d2645dc..7f7bcff 100644
--- a/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
 public class GroupPredicate extends ChangeIndexPredicate {
@@ -25,7 +24,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     for (PatchSet ps : cd.patchSets()) {
       List<String> groups = ps.getGroups();
       if (groups != null && groups.contains(getValue())) {
diff --git a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index e422b74..e57a8b3 100644
--- a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HasDraftByPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.draftsByUser().contains(accountId);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
index b17fffd..0c99cdf 100644
--- a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HasStarsPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.stars().containsKey(accountId);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/java/com/google/gerrit/server/query/change/HashtagPredicate.java
index 95ecf89..1fe4af4 100644
--- a/java/com/google/gerrit/server/query/change/HashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HashtagPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.server.change.HashtagsUtil;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class HashtagPredicate extends ChangeIndexPredicate {
   public HashtagPredicate(String hashtag) {
@@ -26,7 +25,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     for (String hashtag : object.notes().load().getHashtags()) {
       if (hashtag.equalsIgnoreCase(getValue())) {
         return true;
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 973c451..74a0f71 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -91,19 +90,19 @@
     this.notesFactory = notesFactory;
   }
 
-  public List<ChangeData> byKey(Change.Key key) throws OrmException {
+  public List<ChangeData> byKey(Change.Key key) {
     return byKeyPrefix(key.get());
   }
 
-  public List<ChangeData> byKeyPrefix(String prefix) throws OrmException {
+  public List<ChangeData> byKeyPrefix(String prefix) {
     return query(new ChangeIdPredicate(prefix));
   }
 
-  public List<ChangeData> byLegacyChangeId(Change.Id id) throws OrmException {
+  public List<ChangeData> byLegacyChangeId(Change.Id id) {
     return query(new LegacyChangeIdPredicate(id));
   }
 
-  public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) throws OrmException {
+  public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
     List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
       preds.add(new LegacyChangeIdPredicate(id));
@@ -111,12 +110,11 @@
     return query(or(preds));
   }
 
-  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) throws OrmException {
+  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) {
     return query(byBranchKeyPred(branch, key));
   }
 
-  public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key)
-      throws OrmException {
+  public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) {
     return query(and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open()));
   }
 
@@ -129,21 +127,20 @@
     return and(ref(branch), project(branch.getParentKey()), change(key));
   }
 
-  public List<ChangeData> byProject(Project.NameKey project) throws OrmException {
+  public List<ChangeData> byProject(Project.NameKey project) {
     return query(project(project));
   }
 
-  public List<ChangeData> byBranchOpen(Branch.NameKey branch) throws OrmException {
+  public List<ChangeData> byBranchOpen(Branch.NameKey branch) {
     return query(and(ref(branch), project(branch.getParentKey()), open()));
   }
 
-  public List<ChangeData> byBranchNew(Branch.NameKey branch) throws OrmException {
+  public List<ChangeData> byBranchNew(Branch.NameKey branch) {
     return query(and(ref(branch), project(branch.getParentKey()), status(Change.Status.NEW)));
   }
 
   public Iterable<ChangeData> byCommitsOnBranchNotMerged(
-      Repository repo, Branch.NameKey branch, Collection<String> hashes)
-      throws OrmException, IOException {
+      Repository repo, Branch.NameKey branch, Collection<String> hashes) throws IOException {
     return byCommitsOnBranchNotMerged(
         repo,
         branch,
@@ -155,7 +152,7 @@
   @VisibleForTesting
   Iterable<ChangeData> byCommitsOnBranchNotMerged(
       Repository repo, Branch.NameKey branch, Collection<String> hashes, int indexLimit)
-      throws OrmException, IOException {
+      throws IOException {
     if (hashes.size() > indexLimit) {
       return byCommitsOnBranchNotMergedFromDatabase(repo, branch, hashes);
     }
@@ -163,8 +160,7 @@
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
-      Repository repo, Branch.NameKey branch, Collection<String> hashes)
-      throws OrmException, IOException {
+      Repository repo, Branch.NameKey branch, Collection<String> hashes) throws IOException {
     Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
     String lastPrefix = null;
     for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
@@ -188,13 +184,13 @@
             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(n));
   }
 
   private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
-      Branch.NameKey branch, Collection<String> hashes) throws OrmException {
+      Branch.NameKey branch, Collection<String> hashes) {
     return query(
         and(
             ref(branch),
@@ -211,50 +207,45 @@
     return commits;
   }
 
-  public List<ChangeData> byProjectOpen(Project.NameKey project) throws OrmException {
+  public List<ChangeData> byProjectOpen(Project.NameKey project) {
     return query(and(project(project), open()));
   }
 
-  public List<ChangeData> byTopicOpen(String topic) throws OrmException {
+  public List<ChangeData> byTopicOpen(String topic) {
     return query(and(new ExactTopicPredicate(topic), open()));
   }
 
-  public List<ChangeData> byCommit(ObjectId id) throws OrmException {
+  public List<ChangeData> byCommit(ObjectId id) {
     return byCommit(id.name());
   }
 
-  public List<ChangeData> byCommit(String hash) throws OrmException {
+  public List<ChangeData> byCommit(String hash) {
     return query(commit(hash));
   }
 
-  public List<ChangeData> byProjectCommit(Project.NameKey project, ObjectId id)
-      throws OrmException {
+  public List<ChangeData> byProjectCommit(Project.NameKey project, ObjectId id) {
     return byProjectCommit(project, id.name());
   }
 
-  public List<ChangeData> byProjectCommit(Project.NameKey project, String hash)
-      throws OrmException {
+  public List<ChangeData> byProjectCommit(Project.NameKey project, String hash) {
     return query(and(project(project), commit(hash)));
   }
 
-  public List<ChangeData> byProjectCommits(Project.NameKey project, List<String> hashes)
-      throws OrmException {
+  public List<ChangeData> byProjectCommits(Project.NameKey project, List<String> hashes) {
     int n = indexConfig.maxTerms() - 1;
     checkArgument(hashes.size() <= n, "cannot exceed %s commits", n);
     return query(and(project(project), or(commits(hashes))));
   }
 
-  public List<ChangeData> byBranchCommit(String project, String branch, String hash)
-      throws OrmException {
+  public List<ChangeData> byBranchCommit(String project, String branch, String hash) {
     return query(byBranchCommitPred(project, branch, hash));
   }
 
-  public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) throws OrmException {
+  public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) {
     return byBranchCommit(branch.getParentKey().get(), branch.get(), hash);
   }
 
-  public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash)
-      throws OrmException {
+  public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash) {
     return query(and(byBranchCommitPred(project, branch, hash), open()));
   }
 
@@ -268,7 +259,7 @@
     return and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash));
   }
 
-  public List<ChangeData> bySubmissionId(String cs) throws OrmException {
+  public List<ChangeData> bySubmissionId(String cs) {
     if (Strings.isNullOrEmpty(cs)) {
       return Collections.emptyList();
     }
@@ -290,8 +281,7 @@
       Provider<InternalChangeQuery> queryProvider,
       IndexConfig indexConfig,
       Project.NameKey project,
-      Collection<String> groups)
-      throws OrmException {
+      Collection<String> groups) {
     // These queries may be complex along multiple dimensions:
     //  * Many groups per change, if there are very many patch sets. This requires partitioning the
     //    list of predicates and combining results.
diff --git a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 7ff5a28..ddb6f32 100644
--- a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -48,7 +47,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     Set<Account.Id> reviewedBy = cd.reviewedBy();
     return !reviewedBy.isEmpty() ? reviewedBy.contains(id) : id.equals(NOT_REVIEWED);
   }
diff --git a/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java b/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
index 225dc454..27309af 100644
--- a/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class IsUnresolvedPredicate extends IntegerRangeChangePredicate {
   public IsUnresolvedPredicate() throws QueryParseException {
@@ -28,7 +27,7 @@
   }
 
   @Override
-  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+  protected Integer getValueInt(ChangeData changeData) {
     return ChangeField.UNRESOLVED_COMMENT_COUNT.get(changeData);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index b698181..6028f2d 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -81,7 +81,7 @@
       }
     }
     if (r.isEmpty()) {
-      return none();
+      return ImmutableList.of(ChangeIndexPredicate.none());
     } else if (checkIsVisible) {
       return ImmutableList.of(or(r), builder.is_visible());
     } else {
@@ -98,11 +98,6 @@
     return Collections.emptySet();
   }
 
-  protected static List<Predicate<ChangeData>> none() {
-    Predicate<ChangeData> any = any();
-    return ImmutableList.of(not(any));
-  }
-
   @Override
   public int getCost() {
     return 1;
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0cfcedb..0bd8c88 100644
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gwtorm.server.OrmException;
 
 /** Predicate to match changes that contains specified text in commit messages body. */
 public class MessagePredicate extends ChangeIndexPredicate {
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     try {
       Predicate<ChangeData> p = Predicate.and(new LegacyChangeIdPredicate(object.getId()), this);
       for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -40,7 +40,7 @@
         }
       }
     } catch (QueryParseException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
 
     return false;
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index dcbf517..ba06b89 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.ListResultSet;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -35,7 +35,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
+  public ResultSet<ChangeData> read() {
     // TODO(spearce) This probably should be more lazy.
     //
     List<ChangeData> r = new ArrayList<>();
@@ -48,14 +48,14 @@
           }
         }
       } else {
-        throw new OrmException("No ChangeDataSource: " + p);
+        throw new StorageException("No ChangeDataSource: " + p);
       }
     }
     return new ListResultSet<>(r);
   }
 
   @Override
-  public ResultSet<FieldBundle> readRaw() throws OrmException {
+  public ResultSet<FieldBundle> readRaw() {
     throw new UnsupportedOperationException("not implemented");
   }
 
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index c07428b..08e6f33 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -19,6 +19,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -75,6 +75,8 @@
     JSON
   }
 
+  public static final Gson GSON = new Gson();
+
   private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryProcessor queryProcessor;
@@ -116,6 +118,10 @@
     queryProcessor.setUserProvidedLimit(n);
   }
 
+  public void setNoLimit(boolean on) {
+    queryProcessor.setNoLimit(on);
+  }
+
   public void setStart(int n) {
     queryProcessor.setStart(n);
   }
@@ -213,7 +219,7 @@
         stats.moreChanges = results.more();
         stats.runTimeMilliseconds = TimeUtil.nowMs() - stats.runTimeMilliseconds;
         show(stats);
-      } catch (OrmException err) {
+      } catch (StorageException err) {
         logger.atSevere().withCause(err).log("Cannot execute query: %s", queryString);
 
         ErrorMessage m = new ErrorMessage();
@@ -236,7 +242,7 @@
 
   private ChangeAttribute buildChangeAttribute(
       ChangeData d, Map<Project.NameKey, Repository> repos, Map<Project.NameKey, RevWalk> revWalks)
-      throws OrmException, IOException {
+      throws IOException {
     LabelTypes labelTypes = d.getLabelTypes();
     ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), d.notes());
     eventFactory.extend(c, d.change());
@@ -318,7 +324,7 @@
       eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
     }
 
-    c.plugins = queryProcessor.create(d);
+    c.plugins = queryProcessor.getAttributesFactory().create(d);
     return c;
   }
 
@@ -351,7 +357,7 @@
         break;
 
       case JSON:
-        out.print(new Gson().toJson(data));
+        out.print(GSON.toJson(data));
         out.print('\n');
         break;
     }
@@ -393,7 +399,7 @@
       // Idention for multi-line text is
       // current depth indetion + length of field + length of ": "
       indent = indent(indent.length() + field.length() + spacesDepthRatio);
-      out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
+      out.print(((String) value).replace("\n", "\n" + indent).trim());
       out.print('\n');
     } else if (value instanceof Long && isDateField(field)) {
       out.print(' ');
diff --git a/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index ff494fc..100a66c 100644
--- a/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class OwnerPredicate extends ChangeIndexPredicate {
   protected final Account.Id id;
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     return change != null && id.equals(change.getOwner());
   }
diff --git a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index c48bdd5..41b3204 100644
--- a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
 
 public class OwnerinPredicate extends PostFilterPredicate<ChangeData> {
   protected final IdentifiedUser.GenericFactory userFactory;
@@ -31,7 +30,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     final Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index 09a46a4..febd6c6 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectPredicate extends ChangeIndexPredicate {
   public ProjectPredicate(String id) {
@@ -29,7 +28,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
index 28b1302..744f4d2 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectPrefixPredicate extends ChangeIndexPredicate {
   public ProjectPrefixPredicate(String prefix) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change c = object.change();
     return c != null && c.getDest().getParentKey().get().startsWith(getValue());
   }
diff --git a/java/com/google/gerrit/server/query/change/RefPredicate.java b/java/com/google/gerrit/server/query/change/RefPredicate.java
index c9314e4..2ed4c99 100644
--- a/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class RefPredicate extends ChangeIndexPredicate {
   public RefPredicate(String ref) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
new file mode 100644
index 0000000..1787c76
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+public class RegexDirectoryPredicate extends ChangeRegexPredicate {
+  protected final RunAutomaton pattern;
+
+  public RegexDirectoryPredicate(String re) {
+    super(ChangeField.DIRECTORY, re);
+
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getDirectories(cd).stream().anyMatch(pattern::run);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index f694904..4c3c04c 100644
--- a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -16,9 +16,6 @@
 
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.ioutil.RegexListSearcher;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import java.util.List;
 
 public class RegexPathPredicate extends ChangeRegexPredicate {
   public RegexPathPredicate(String re) {
@@ -26,14 +23,11 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    List<String> files;
-    try {
-      files = object.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return RegexListSearcher.ofStrings(getValue()).search(files).findAny().isPresent();
+  public boolean match(ChangeData object) {
+    return RegexListSearcher.ofStrings(getValue())
+        .search(object.currentFilePaths())
+        .findAny()
+        .isPresent();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index 1efc77d..46a17f6 100644
--- a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -39,7 +38,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index 92abafb..af211e6 100644
--- a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 2b58c88..0441afa 100644
--- a/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
@@ -39,7 +38,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null || change.getTopic() == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
index 7f4ade0..eea1b1e 100644
--- a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class RevertOfPredicate extends ChangeIndexPredicate {
   public RevertOfPredicate(String revertOf) {
@@ -23,7 +22,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     if (cd.change().getRevertOf() == null) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
index 667c630..070f800 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gwtorm.server.OrmException;
 
 class ReviewerByEmailPredicate extends ChangeIndexPredicate {
 
@@ -43,7 +42,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.reviewersByEmail().asTable().get(state, adr) != null;
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 0d1ae44..19104d3 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -20,7 +20,6 @@
 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.gwtorm.server.OrmException;
 
 public class ReviewerPredicate extends ChangeIndexPredicate {
   protected static Predicate<ChangeData> forState(Account.Id id, ReviewerStateInternal state) {
@@ -50,7 +49,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.reviewers().asTable().get(state, id) != null;
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index a0aa8b5..542a35780 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gwtorm.server.OrmException;
 
 public class ReviewerinPredicate extends PostFilterPredicate<ChangeData> {
   protected final IdentifiedUser.GenericFactory userFactory;
@@ -36,7 +35,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     for (Account.Id accountId : object.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
       IdentifiedUser reviewer = userFactory.create(accountId);
       if (reviewer.getEffectiveGroups().contains(uuid)) {
diff --git a/java/com/google/gerrit/server/query/change/StarPredicate.java b/java/com/google/gerrit/server/query/change/StarPredicate.java
index 12d4753..6c5fd78 100644
--- a/java/com/google/gerrit/server/query/change/StarPredicate.java
+++ b/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class StarPredicate extends ChangeIndexPredicate {
   protected final Account.Id accountId;
@@ -30,7 +29,7 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) {
     return cd.stars().get(accountId).contains(label);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 5fdeb68..0995a59 100644
--- a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class SubmissionIdPredicate extends ChangeIndexPredicate {
   public SubmissionIdPredicate(String changeSet) {
@@ -24,7 +23,7 @@
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) {
     Change change = object.change();
     if (change == null) {
       return false;
diff --git a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index 17034df..e59ae43 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 import java.util.Set;
 
 public class SubmitRecordPredicate extends ChangeIndexPredicate {
@@ -31,8 +30,7 @@
       return new SubmitRecordPredicate(status.name() + ',' + lowerLabel);
     }
     return Predicate.or(
-        accounts
-            .stream()
+        accounts.stream()
             .map(a -> new SubmitRecordPredicate(status.name() + ',' + lowerLabel + ',' + a.get()))
             .collect(toList()));
   }
@@ -42,7 +40,7 @@
   }
 
   @Override
-  public boolean match(ChangeData in) throws OrmException {
+  public boolean match(ChangeData in) {
     return ChangeField.formatSubmitRecordValues(in).contains(getValue());
   }
 
diff --git a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
index df78315..c507f1c 100644
--- a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
 
 public class SubmittablePredicate extends ChangeIndexPredicate {
   protected final SubmitRecord.Status status;
@@ -27,9 +26,8 @@
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
-    return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT)
-        .stream()
+  public boolean match(ChangeData cd) {
+    return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream()
         .anyMatch(r -> r.status == status);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index 4f751c5..622fa2c 100644
--- a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -14,26 +14,16 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
 
 public class TrackingIdPredicate extends ChangeIndexPredicate {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   public TrackingIdPredicate(String trackingId) {
     super(ChangeField.TR, trackingId);
   }
 
   @Override
-  public boolean match(ChangeData cd) throws OrmException {
-    try {
-      return cd.trackingFooters().containsValue(getValue());
-    } catch (IOException e) {
-      logger.atWarning().withCause(e).log("Cannot extract footers from %s", cd.getId());
-    }
-    return false;
+  public boolean match(ChangeData cd) {
+    return cd.trackingFooters().containsValue(getValue());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
index 144a81c..8248bf5 100644
--- a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.query.group;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 
 public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<InternalGroup> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -38,7 +37,7 @@
   }
 
   @Override
-  public boolean match(InternalGroup group) throws OrmException {
+  public boolean match(InternalGroup group) {
     try {
       boolean canSee = groupControlFactory.controlFor(user, group.getGroupUUID()).isVisible();
       if (!canSee) {
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 296dc17..215e36f 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -24,14 +24,15 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
@@ -40,7 +41,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Parses a query string meant to be applied to group objects. */
-public class GroupQueryBuilder extends QueryBuilder<InternalGroup> {
+public class GroupQueryBuilder extends QueryBuilder<InternalGroup, GroupQueryBuilder> {
   public static final String FIELD_UUID = "uuid";
   public static final String FIELD_DESCRIPTION = "description";
   public static final String FIELD_INNAME = "inname";
@@ -68,7 +69,7 @@
 
   @Inject
   GroupQueryBuilder(Arguments args) {
-    super(mydef);
+    super(mydef, null);
     this.args = args;
   }
 
@@ -133,7 +134,7 @@
 
   @Operator
   public Predicate<InternalGroup> member(String query)
-      throws QueryParseException, OrmException, ConfigInvalidException, IOException {
+      throws QueryParseException, ConfigInvalidException, IOException {
     Set<Account.Id> accounts = parseAccount(query);
     List<Predicate<InternalGroup>> predicates =
         accounts.stream().map(GroupPredicates::member).collect(toImmutableList());
@@ -156,12 +157,15 @@
   }
 
   private Set<Account.Id> parseAccount(String nameOrEmail)
-      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
-    if (foundAccounts.isEmpty()) {
-      throw error("User " + nameOrEmail + " not found");
+      throws QueryParseException, IOException, ConfigInvalidException {
+    try {
+      return args.accountResolver.resolve(nameOrEmail).asNonEmptyIdSet();
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new QueryRequiresAuthException(e.getMessage(), e);
+      }
+      throw new QueryParseException(e.getMessage(), e);
     }
-    return foundAccounts;
   }
 
   private AccountGroup.UUID parseGroup(String groupNameOrUuid) throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index 42b38bf..5749809 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Optional;
@@ -46,24 +45,24 @@
     super(queryProcessor, indexes, indexConfig);
   }
 
-  public Optional<InternalGroup> byName(AccountGroup.NameKey groupName) throws OrmException {
+  public Optional<InternalGroup> byName(AccountGroup.NameKey groupName) {
     return getOnlyGroup(GroupPredicates.name(groupName.get()), "group name '" + groupName + "'");
   }
 
-  public Optional<InternalGroup> byId(AccountGroup.Id groupId) throws OrmException {
+  public Optional<InternalGroup> byId(AccountGroup.Id groupId) {
     return getOnlyGroup(GroupPredicates.id(groupId), "group id '" + groupId + "'");
   }
 
-  public List<InternalGroup> byMember(Account.Id memberId) throws OrmException {
+  public List<InternalGroup> byMember(Account.Id memberId) {
     return query(GroupPredicates.member(memberId));
   }
 
-  public List<InternalGroup> bySubgroup(AccountGroup.UUID subgroupId) throws OrmException {
+  public List<InternalGroup> bySubgroup(AccountGroup.UUID subgroupId) {
     return query(GroupPredicates.subgroup(subgroupId));
   }
 
   private Optional<InternalGroup> getOnlyGroup(
-      Predicate<InternalGroup> predicate, String groupDescription) throws OrmException {
+      Predicate<InternalGroup> predicate, String groupDescription) {
     List<InternalGroup> groups = query(predicate);
     if (groups.isEmpty()) {
       return Optional.empty();
diff --git a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
index b1c5af0..2c84b9a 100644
--- a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 
 public class ProjectIsVisibleToPredicate extends IsVisibleToPredicate<ProjectData> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -37,7 +36,7 @@
   }
 
   @Override
-  public boolean match(ProjectData pd) throws OrmException {
+  public boolean match(ProjectData pd) {
     if (!pd.getProject().getState().permitsRead()) {
       logger.atFine().log("Filter out non-readable project: %s", pd);
       return false;
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 2e406aa..5f13236 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -27,6 +27,10 @@
     return new ProjectPredicate(ProjectField.NAME, nameKey.get());
   }
 
+  public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
+    return new ProjectPredicate(ProjectField.PARENT_NAME, parentNameKey.get());
+  }
+
   public static Predicate<ProjectData> inname(String name) {
     return new ProjectPredicate(ProjectField.NAME_PART, name.toLowerCase(Locale.US));
   }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index 872d3e0..570da6b 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -28,7 +28,7 @@
 import java.util.List;
 
 /** Parses a query string meant to be applied to project objects. */
-public class ProjectQueryBuilder extends QueryBuilder<ProjectData> {
+public class ProjectQueryBuilder extends QueryBuilder<ProjectData, ProjectQueryBuilder> {
   public static final String FIELD_LIMIT = "limit";
 
   private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilder> mydef =
@@ -36,7 +36,7 @@
 
   @Inject
   ProjectQueryBuilder() {
-    super(mydef);
+    super(mydef, null);
   }
 
   @Operator
@@ -45,6 +45,11 @@
   }
 
   @Operator
+  public Predicate<ProjectData> parent(String parentName) {
+    return ProjectPredicates.parent(new Project.NameKey(parentName));
+  }
+
+  @Operator
   public Predicate<ProjectData> inname(String namePart) {
     if (namePart.isEmpty()) {
       return name(namePart);
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
index c6e67ca..479bb52 100644
--- a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -20,15 +20,15 @@
 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.NameKey;
+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;
-import javax.inject.Inject;
-import javax.inject.Provider;
-import javax.inject.Singleton;
 
 @Singleton
 public class DefaultQuotaBackend implements QuotaBackend {
@@ -125,13 +125,13 @@
     }
 
     @Override
-    public QuotaBackend.WithResource project(NameKey project) {
+    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, NameKey project) {
+    public QuotaBackend.WithResource change(Change.Id change, Project.NameKey project) {
       QuotaRequestContext ctx = requestContext.toBuilder().change(change).project(project).build();
       return new WithResource(quotaEnforcers, ctx);
     }
diff --git a/java/com/google/gerrit/server/quota/QuotaResponse.java b/java/com/google/gerrit/server/quota/QuotaResponse.java
index 9a8a515..99a7c1d 100644
--- a/java/com/google/gerrit/server/quota/QuotaResponse.java
+++ b/java/com/google/gerrit/server/quota/QuotaResponse.java
@@ -101,8 +101,7 @@
     }
 
     public String errorMessage() {
-      return error()
-          .stream()
+      return error().stream()
           .map(QuotaResponse::message)
           .flatMap(Streams::stream)
           .collect(Collectors.joining(", "));
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 0477a63..3040644 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
@@ -27,7 +28,6 @@
         "//lib:blame-cache",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/server/restapi/access/ListAccess.java b/java/com/google/gerrit/server/restapi/access/ListAccess.java
index 3f01c6c..a3e9530 100644
--- a/java/com/google/gerrit/server/restapi/access/ListAccess.java
+++ b/java/com/google/gerrit/server/restapi/access/ListAccess.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.GetAccess;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -50,7 +49,7 @@
   @Override
   public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
       throws ResourceNotFoundException, ResourceConflictException, IOException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     Map<String, ProjectAccessInfo> access = new TreeMap<>();
     for (String p : projects) {
       access.put(p, getAccess.apply(new Project.NameKey(p)));
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index c301ab2..119e2e4 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -21,11 +21,9 @@
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -35,32 +33,30 @@
 @Singleton
 public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource> {
   private final AccountResolver accountResolver;
-  private final AccountControl.Factory accountControlFactory;
   private final Provider<QueryAccounts> list;
   private final DynamicMap<RestView<AccountResource>> views;
 
   @Inject
   public AccountsCollection(
       AccountResolver accountResolver,
-      AccountControl.Factory accountControlFactory,
       Provider<QueryAccounts> list,
       DynamicMap<RestView<AccountResource>> views) {
     this.accountResolver = accountResolver;
-    this.accountControlFactory = accountControlFactory;
     this.list = list;
     this.views = views;
   }
 
   @Override
   public AccountResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, AuthException, OrmException, IOException,
-          ConfigInvalidException {
-    IdentifiedUser user = accountResolver.parseId(id.get());
-    if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
-      throw new ResourceNotFoundException(
-          String.format("Account '%s' is not found or ambiguous", id));
+      throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
+    try {
+      return new AccountResource(accountResolver.resolve(id.get()).asUniqueUser());
+    } catch (UnresolvableAccountException e) {
+      if (e.isSelf()) {
+        throw new AuthException(e.getMessage(), e);
+      }
+      throw new ResourceNotFoundException(e.getMessage(), e);
     }
-    return new AccountResource(user);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index e47540d..7c05f4e 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -18,8 +18,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteSource;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -72,7 +71,7 @@
 
   @Override
   public Response<SshKeyInfo> apply(AccountResource rsrc, SshKeyInput input)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 2f9f2a9..b19994c 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -36,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;
@@ -49,10 +48,10 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -103,13 +102,13 @@
   public Response<AccountInfo> apply(
       TopLevelResource rsrc, IdString id, @Nullable AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+          IOException, ConfigInvalidException, PermissionBackendException {
     return apply(id, input != null ? input : new AccountInput());
   }
 
   public Response<AccountInfo> apply(IdString id, AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+          IOException, ConfigInvalidException, PermissionBackendException {
     String username = id.get();
     if (input.username != null && !username.equals(input.username)) {
       throw new BadRequestException("username must match URL");
@@ -189,7 +188,7 @@
   }
 
   private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index e4e8525..cc4cf21 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -84,7 +83,7 @@
 
   @Override
   public Response<EmailInfo> apply(AccountResource rsrc, IdString id, EmailInput input)
-      throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+      throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
     if (input == null) {
       input = new EmailInput();
@@ -103,7 +102,7 @@
 
   /** To be used from plugins that want to create emails without permission checks. */
   public Response<EmailInfo> apply(IdentifiedUser user, IdString id, EmailInput input)
-      throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+      throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
     String email = id.get().trim();
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteActive.java b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
index 4302513..ffd7893 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteActive.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.SetInactiveFlag;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,7 +45,7 @@
 
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, IOException, ConfigInvalidException {
     if (self.get().hasSameAccountId(rsrc.getUser())) {
       throw new ResourceConflictException("cannot deactivate own account");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 87564ab..4788301 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -32,7 +32,6 @@
 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;
@@ -57,7 +56,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -111,7 +109,7 @@
   @Override
   public ImmutableList<DeletedDraftCommentInfo> apply(
       AccountResource rsrc, DeleteDraftCommentsInput input)
-      throws RestApiException, OrmException, UpdateException {
+      throws RestApiException, UpdateException {
     CurrentUser user = userProvider.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
@@ -170,14 +168,14 @@
     private final Account.Id accountId;
     private DeletedDraftCommentInfo result;
 
-    Op(CommentFormatter commentFormatter, Id accountId) {
+    Op(CommentFormatter commentFormatter, Account.Id accountId) {
       this.commentFormatter = commentFormatter;
       this.accountId = accountId;
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException, PermissionBackendException {
+        throws PatchListNotAvailableException, PermissionBackendException {
       ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder();
       boolean dirty = false;
       for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), accountId)) {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index f0269f1..7a03005 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +68,7 @@
   @Override
   public Response<?> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, ResourceConflictException,
-          MethodNotAllowedException, OrmException, IOException, ConfigInvalidException,
+          MethodNotAllowedException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
@@ -79,15 +78,13 @@
 
   public Response<?> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
-          OrmException, IOException, ConfigInvalidException {
+          IOException, ConfigInvalidException {
     if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
 
     Set<ExternalId> extIds =
-        externalIds
-            .byAccount(user.getAccountId())
-            .stream()
+        externalIds.byAccount(user.getAccountId()).stream()
             .filter(e -> email.equals(e.email()))
             .collect(toSet());
     if (extIds.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 05b1771..82b445f 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -66,8 +65,7 @@
 
   @Override
   public Response<?> apply(AccountResource resource, List<String> extIds)
-      throws RestApiException, IOException, OrmException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
@@ -77,9 +75,7 @@
     }
 
     Map<ExternalId.Key, ExternalId> externalIdMap =
-        externalIds
-            .byAccount(resource.getUser().getAccountId())
-            .stream()
+        externalIds.byAccount(resource.getUser().getAccountId()).stream()
             .collect(toMap(ExternalId::key, Function.identity()));
 
     List<ExternalId> toDelete = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index b7b3c83..787e083 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,8 +54,8 @@
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index 0e2edb9..798aad1 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -59,8 +58,8 @@
 
   @Override
   public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, UnprocessableEntityException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
@@ -76,8 +75,7 @@
             accountId,
             u ->
                 u.deleteProjectWatches(
-                    input
-                        .stream()
+                    input.stream()
                         .filter(Objects::nonNull)
                         .map(w -> ProjectWatchKey.create(new Project.NameKey(w.project), w.filter))
                         .collect(toList())));
diff --git a/java/com/google/gerrit/server/restapi/account/GetAccount.java b/java/com/google/gerrit/server/restapi/account/GetAccount.java
index 6b73ae3b..6544f8d 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAccount.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public AccountInfo apply(AccountResource rsrc) throws OrmException, PermissionBackendException {
+  public AccountInfo apply(AccountResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getUser().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 9de4a00..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;
@@ -113,7 +113,7 @@
     for (String pluginName : pluginCapabilities.plugins()) {
       for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
         PluginPermission p = new PluginPermission(pluginName, capability);
-        if (want(pluginPermissionName(p))) {
+        if (want(pluginCapabilityName(p))) {
           toTest.add(p);
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index 97d0c60..72044c4 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collections;
@@ -37,8 +36,7 @@
   }
 
   @Override
-  public AccountDetailInfo apply(AccountResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public AccountDetailInfo apply(AccountResource rsrc) throws PermissionBackendException {
     Account a = rsrc.getUser().getAccount();
     AccountDetailInfo info = new AccountDetailInfo(a.getId().get());
     info.registeredOn = a.getRegisteredOn();
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index ed3347f..8c21536 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -48,9 +48,7 @@
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
-    return rsrc.getUser()
-        .getEmailAddresses()
-        .stream()
+    return rsrc.getUser().getEmailAddresses().stream()
         .filter(Objects::nonNull)
         .map(e -> toEmailInfo(rsrc, e))
         .sorted(comparing((EmailInfo e) -> e.email))
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 7a420ab..ef448dc 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -60,7 +59,7 @@
 
   @Override
   public List<AccountExternalIdInfo> apply(AccountResource resource)
-      throws RestApiException, IOException, OrmException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index ad9746e..569ff76 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -43,8 +42,7 @@
   }
 
   @Override
-  public List<GroupInfo> apply(AccountResource resource)
-      throws OrmException, PermissionBackendException {
+  public List<GroupInfo> apply(AccountResource resource) throws PermissionBackendException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
     Set<AccountGroup.UUID> knownGroups = user.getEffectiveGroups().getKnownGroups();
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index a49f9df..408aa5f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,8 +54,8 @@
 
   @Override
   public List<SshKeyInfo> apply(AccountResource rsrc)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 61021be..fce324e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,18 +56,15 @@
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
-      throws OrmException, AuthException, IOException, ConfigInvalidException,
-          PermissionBackendException, ResourceNotFoundException {
+      throws AuthException, IOException, ConfigInvalidException, PermissionBackendException,
+          ResourceNotFoundException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountState account = accounts.get(accountId).orElseThrow(ResourceNotFoundException::new);
-    return account
-        .getProjectWatches()
-        .entrySet()
-        .stream()
+    return account.getProjectWatches().entrySet().stream()
         .map(e -> toProjectWatchInfo(e.getKey(), e.getValue()))
         .sorted(
             comparing((ProjectWatchInfo pwi) -> pwi.project)
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index 9b012f7..f41764d 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -118,7 +118,7 @@
   @ServerInitiated
   AccountsUpdate provideServerInitiatedAccountsUpdate(
       AccountsUpdate.Factory accountsUpdateFactory, ExternalIdNotes.Factory extIdNotesFactory) {
-    return accountsUpdateFactory.create(null, extIdNotesFactory);
+    return accountsUpdateFactory.createWithServerIdent(extIdNotesFactory);
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index f29a0eb..14bd492 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -66,8 +65,7 @@
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutActive.java b/java/com/google/gerrit/server/restapi/account/PutActive.java
index 8255781..a6ffaa6 100644
--- a/java/com/google/gerrit/server/restapi/account/PutActive.java
+++ b/java/com/google/gerrit/server/restapi/account/PutActive.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.SetInactiveFlag;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, IOException, ConfigInvalidException {
     return setInactiveFlag.activate(rsrc.getUser().getAccountId());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutAgreement.java b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
index 0d8a816..7bac359 100644
--- a/java/com/google/gerrit/server/restapi/account/PutAgreement.java
+++ b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
@@ -17,7 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.accounts.AgreementInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.extensions.events.AgreementSignup;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.group.AddMembers;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -67,7 +66,7 @@
 
   @Override
   public Response<String> apply(AccountResource resource, AgreementInput input)
-      throws IOException, OrmException, RestApiException, ConfigInvalidException {
+      throws IOException, RestApiException, ConfigInvalidException {
     if (!agreementsEnabled) {
       throw new MethodNotAllowedException("contributor agreements disabled");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index a465002..4b223ed 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;
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -75,8 +74,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, HttpPasswordInput input)
-      throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
-          IOException, ConfigInvalidException, PermissionBackendException {
+      throws AuthException, ResourceNotFoundException, ResourceConflictException, IOException,
+          ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
@@ -100,7 +99,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, String newPassword)
-      throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
+      throws ResourceNotFoundException, ResourceConflictException, IOException,
           ConfigInvalidException {
     String userName =
         user.getUserName().orElseThrow(() -> new ResourceConflictException("username must be set"));
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index 1e00aac..30dfc66 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -60,8 +59,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, NameInput input)
-      throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-          IOException, PermissionBackendException, ConfigInvalidException {
+      throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+          PermissionBackendException, ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -70,7 +69,7 @@
 
   public Response<String> apply(IdentifiedUser user, NameInput input)
       throws MethodNotAllowedException, ResourceNotFoundException, IOException,
-          ConfigInvalidException, OrmException {
+          ConfigInvalidException {
     if (input == null) {
       input = new NameInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index a828987..b8edec3 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,8 +68,7 @@
 
   @Override
   public Response<String> apply(AccountResource.Email rsrc, Input input)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -78,7 +76,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, String preferredEmail)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException {
     AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
     AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
     accountsUpdateProvider
@@ -93,8 +91,7 @@
                 // check if the user has a matching email
                 String matchingEmail = null;
                 for (String email :
-                    a.getExternalIds()
-                        .stream()
+                    a.getExternalIds().stream()
                         .map(ExternalId::email)
                         .filter(Objects::nonNull)
                         .collect(toSet())) {
@@ -121,8 +118,7 @@
                               + " by the following account(s): %s",
                           preferredEmail,
                           user.getAccountId(),
-                          existingExtIdsWithThisEmail
-                              .stream()
+                          existingExtIdsWithThisEmail.stream()
                               .map(ExternalId::accountId)
                               .collect(toList()));
                       exception.set(
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 9aee0a3..4f1128d 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -54,8 +53,8 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, StatusInput input)
-      throws AuthException, ResourceNotFoundException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException {
+      throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException,
+          ConfigInvalidException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -63,7 +62,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, StatusInput input)
-      throws ResourceNotFoundException, IOException, ConfigInvalidException, OrmException {
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new StatusInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 856a5db..bc1ffc8 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,8 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,7 +72,7 @@
   @Override
   public String apply(AccountResource rsrc, UsernameInput input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
-          ResourceConflictException, OrmException, IOException, ConfigInvalidException,
+          ResourceConflictException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -108,7 +107,7 @@
               "Set Username via API",
               accountId,
               u -> u.addExternalId(ExternalId.create(key, accountId, null, null)));
-    } catch (OrmDuplicateKeyException dupeErr) {
+    } catch (DuplicateKeyException dupeErr) {
       // If we are using this identity, don't report the exception.
       Optional<ExternalId> other = externalIds.get(key);
       if (other.isPresent() && other.get().accountId().equals(accountId)) {
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 2c0512c..e10d8bf 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.ListAccountsOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -40,7 +41,6 @@
 import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
 import com.google.gerrit.server.query.account.AccountQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -97,7 +97,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(
@@ -148,7 +148,7 @@
 
   @Override
   public List<AccountInfo> apply(TopLevelResource rsrc)
-      throws OrmException, RestApiException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index f4fa354..ee72ab7 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,7 +55,7 @@
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo input)
       throws RestApiException, ConfigInvalidException, RepositoryNotFoundException, IOException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index 4e3f1d5..27d32f2 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,7 +56,7 @@
   @Override
   public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo input)
       throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 2471689..c6623db 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -62,8 +61,7 @@
 
   @Override
   public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo input)
-      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
-          OrmException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SshKeys.java b/java/com/google/gerrit/server/restapi/account/SshKeys.java
index 4e44c71..6e3f905 100644
--- a/java/com/google/gerrit/server/restapi/account/SshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/SshKeys.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -64,7 +63,7 @@
 
   @Override
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException,
+      throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       try {
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index a109815..3c14ad3 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -40,8 +42,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.QueryChanges;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,7 +68,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -114,7 +114,7 @@
 
     @Override
     public Response<?> apply(AccountResource rsrc, IdString id, EmptyInput in)
-        throws RestApiException, OrmException, IOException {
+        throws RestApiException, IOException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to add starred change");
       }
@@ -124,7 +124,7 @@
         change = changes.parse(TopLevelResource.INSTANCE, id);
       } catch (ResourceNotFoundException e) {
         throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
-      } catch (OrmException | PermissionBackendException | IOException e) {
+      } catch (StorageException | PermissionBackendException | IOException e) {
         logger.atSevere().withCause(e).log("cannot resolve change");
         throw new UnprocessableEntityException("internal server error");
       }
@@ -140,7 +140,7 @@
         throw new ResourceConflictException(e.getMessage());
       } catch (IllegalLabelException e) {
         throw new BadRequestException(e.getMessage());
-      } catch (OrmDuplicateKeyException e) {
+      } catch (DuplicateKeyException e) {
         return Response.none();
       }
       return Response.none();
@@ -179,7 +179,7 @@
 
     @Override
     public Response<?> apply(AccountResource.StarredChange rsrc, EmptyInput in)
-        throws AuthException, OrmException, IOException, IllegalLabelException {
+        throws AuthException, IOException, IllegalLabelException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed remove starred change");
       }
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index 5c4c4d5..c610adf 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.QueryChanges;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,7 +67,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
@@ -99,7 +98,7 @@
     @Override
     @SuppressWarnings("unchecked")
     public List<ChangeInfo> apply(AccountResource rsrc)
-        throws BadRequestException, AuthException, OrmException, PermissionBackendException {
+        throws BadRequestException, AuthException, PermissionBackendException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
@@ -121,7 +120,7 @@
     }
 
     @Override
-    public SortedSet<String> apply(AccountResource.Star rsrc) throws AuthException, OrmException {
+    public SortedSet<String> apply(AccountResource.Star rsrc) throws AuthException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to get stars of another account");
       }
@@ -142,7 +141,7 @@
 
     @Override
     public Collection<String> apply(AccountResource.Star rsrc, StarsInput in)
-        throws AuthException, BadRequestException, OrmException {
+        throws AuthException, BadRequestException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to update stars of another account");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 851752d..c3327e4 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,16 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -31,7 +28,7 @@
 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;
@@ -40,7 +37,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -53,7 +49,7 @@
 
   private final ChangeJson.Factory json;
   private final AbandonOp.Factory abandonOpFactory;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final PatchSetUtil patchSetUtil;
 
   @Inject
@@ -61,20 +57,20 @@
       ChangeJson.Factory json,
       RetryHelper retryHelper,
       AbandonOp.Factory abandonOpFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       PatchSetUtil patchSetUtil) {
     super(retryHelper);
     this.json = json;
     this.abandonOpFactory = abandonOpFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.patchSetUtil = patchSetUtil;
   }
 
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
-          IOException, ConfigInvalidException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException,
+          ConfigInvalidException {
     // Not allowed to abandon if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
@@ -87,8 +83,7 @@
             rsrc.getNotes(),
             rsrc.getUser(),
             input.message,
-            notify,
-            notifyUtil.resolveAccounts(input.notifyDetails));
+            notifyResolver.resolve(notify, input.notifyDetails));
     return json.noOptions().format(change);
   }
 
@@ -103,8 +98,7 @@
         notes,
         user,
         "",
-        defaultNotify(notes.getChange()),
-        ImmutableListMultimap.of());
+        NotifyResolver.Result.create(defaultNotify(notes.getChange())));
   }
 
   public Change abandon(
@@ -115,8 +109,7 @@
         notes,
         user,
         msgTxt,
-        defaultNotify(notes.getChange()),
-        ImmutableListMultimap.of());
+        NotifyResolver.Result.create(defaultNotify(notes.getChange())));
   }
 
   public Change abandon(
@@ -124,12 +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);
+    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();
@@ -144,7 +137,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       return description;
     }
 
@@ -152,7 +145,7 @@
       if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyFix.java b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
index e4940ec..231d356 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -67,8 +66,8 @@
 
   @Override
   public Response<EditInfo> apply(FixResource fixResource, Void nothing)
-      throws AuthException, OrmException, ResourceConflictException, IOException,
-          ResourceNotFoundException, PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, ResourceNotFoundException,
+          PermissionBackendException {
     RevisionResource revisionResource = fixResource.getRevisionResource();
     Project.NameKey project = revisionResource.getProject();
     ProjectState projectState = projectCache.checkedGet(project);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 0b1651d..424f4d7 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -54,7 +54,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -95,7 +94,7 @@
 
   @Override
   public ChangeEditResource parse(ChangeResource rsrc, IdString id)
-      throws ResourceNotFoundException, AuthException, IOException, OrmException {
+      throws ResourceNotFoundException, AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (!edit.isPresent()) {
       throw new ResourceNotFoundException(id);
@@ -119,8 +118,7 @@
 
     @Override
     public Response<?> apply(ChangeResource resource, IdString id, Put.Input input)
-        throws AuthException, ResourceConflictException, IOException, OrmException,
-            PermissionBackendException {
+        throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
       putEdit.apply(resource, id.get(), input.content);
       return Response.none();
     }
@@ -137,8 +135,7 @@
 
     @Override
     public Response<?> apply(ChangeResource rsrc, IdString id, Input in)
-        throws IOException, AuthException, ResourceConflictException, OrmException,
-            PermissionBackendException {
+        throws IOException, AuthException, ResourceConflictException, PermissionBackendException {
       return deleteContent.apply(rsrc, id.get());
     }
   }
@@ -151,14 +148,24 @@
     private final FileInfoJson fileInfoJson;
     private final Revisions revisions;
 
+    private String base;
+    private boolean list;
+    private boolean downloadCommands;
+
     @Option(name = "--base", metaVar = "revision-id")
-    String base;
+    public void setBase(String base) {
+      this.base = base;
+    }
 
     @Option(name = "--list")
-    boolean list;
+    public void setList(boolean list) {
+      this.list = list;
+    }
 
     @Option(name = "--download-commands")
-    boolean downloadCommands;
+    public void setDownloadCommands(boolean downloadCommands) {
+      this.downloadCommands = downloadCommands;
+    }
 
     @Inject
     Detail(
@@ -174,8 +181,7 @@
 
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc)
-        throws AuthException, IOException, ResourceNotFoundException, OrmException,
-            PermissionBackendException {
+        throws AuthException, IOException, ResourceNotFoundException, PermissionBackendException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         return Response.none();
@@ -230,8 +236,7 @@
 
     @Override
     public Response<?> apply(ChangeResource resource, Post.Input input)
-        throws AuthException, IOException, ResourceConflictException, OrmException,
-            PermissionBackendException {
+        throws AuthException, IOException, ResourceConflictException, PermissionBackendException {
       Project.NameKey project = resource.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
         if (isRestoreFile(input)) {
@@ -276,14 +281,12 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, IOException, OrmException,
-            PermissionBackendException {
+        throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath(), input.content);
     }
 
     public Response<?> apply(ChangeResource rsrc, String path, RawInput newContent)
-        throws ResourceConflictException, AuthException, IOException, OrmException,
-            PermissionBackendException {
+        throws ResourceConflictException, AuthException, IOException, PermissionBackendException {
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
       }
@@ -317,14 +320,12 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, OrmException, IOException,
-            PermissionBackendException {
+        throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath());
     }
 
     public Response<?> apply(ChangeResource rsrc, String filePath)
-        throws AuthException, IOException, OrmException, ResourceConflictException,
-            PermissionBackendException {
+        throws AuthException, IOException, ResourceConflictException, PermissionBackendException {
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
         editModifier.deleteFile(repository, rsrc.getNotes(), filePath);
       } catch (InvalidChangeOperationException e) {
@@ -419,7 +420,7 @@
     @Override
     public Object apply(ChangeResource rsrc, Input input)
         throws AuthException, IOException, BadRequestException, ResourceConflictException,
-            OrmException, PermissionBackendException {
+            PermissionBackendException {
       if (input == null || Strings.isNullOrEmpty(input.message)) {
         throw new BadRequestException("commit message must be provided");
       }
@@ -453,7 +454,7 @@
 
     @Override
     public BinaryResult apply(ChangeResource rsrc)
-        throws AuthException, IOException, ResourceNotFoundException, OrmException {
+        throws AuthException, IOException, ResourceNotFoundException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       String msg;
       if (edit.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
index 59b3111..6ec4fdb 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
@@ -21,7 +21,6 @@
 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.Singleton;
 import java.io.IOException;
@@ -38,8 +37,7 @@
   }
 
   @Override
-  public IncludedInInfo apply(ChangeResource rsrc)
-      throws RestApiException, OrmException, IOException {
+  public IncludedInInfo apply(ChangeResource rsrc) throws RestApiException, IOException {
     PatchSet ps = psUtil.current(rsrc.getNotes());
     return includedIn.apply(rsrc.getProject(), ps.getRevision().get());
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
index 25fc350..96c517f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -52,7 +51,7 @@
 
   @Override
   public ChangeMessageResource parse(ChangeResource parent, IdString id)
-      throws OrmException, ResourceNotFoundException, PermissionBackendException {
+      throws ResourceNotFoundException, PermissionBackendException {
     String uuid = id.get();
 
     List<ChangeMessageInfo> changeMessages = listChangeMessages.apply(parent);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 9972195..9f2a52c 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -81,7 +80,7 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException, IOException {
+      throws RestApiException, PermissionBackendException, IOException {
     List<ChangeNotes> notes = changeFinder.find(id.encoded(), true);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(id);
@@ -98,8 +97,8 @@
   }
 
   public ChangeResource parse(Change.Id id)
-      throws ResourceConflictException, ResourceNotFoundException, OrmException,
-          PermissionBackendException, IOException {
+      throws ResourceConflictException, ResourceNotFoundException, PermissionBackendException,
+          IOException {
     List<ChangeNotes> notes = changeFinder.find(id);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
diff --git a/java/com/google/gerrit/server/restapi/change/Check.java b/java/com/google/gerrit/server/restapi/change/Check.java
index dd0ce10..f62aa5a 100644
--- a/java/com/google/gerrit/server/restapi/change/Check.java
+++ b/java/com/google/gerrit/server/restapi/change/Check.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -47,14 +46,13 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws RestApiException, OrmException {
+  public Response<ChangeInfo> apply(ChangeResource rsrc) throws RestApiException {
     return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
-      throws RestApiException, OrmException, PermissionBackendException, NoSuchProjectException,
-          IOException {
+      throws RestApiException, PermissionBackendException, NoSuchProjectException, IOException {
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
     if (!rsrc.isUserOwner()) {
       try {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index b68122e..72781c3 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -78,8 +77,8 @@
   @Override
   public CherryPickChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
-      throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, UpdateException, RestApiException, PermissionBackendException,
+          ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
     if (input.message == null || input.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index cbf570f..19a9233 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,7 +31,6 @@
 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.server.ApprovalsUtil;
@@ -36,9 +38,8 @@
 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;
@@ -46,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;
@@ -57,7 +59,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -103,7 +104,7 @@
   private final ChangeNotes.Factory changeNotesFactory;
   private final ProjectCache projectCache;
   private final ApprovalsUtil approvalsUtil;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   CherryPickChange(
@@ -118,7 +119,7 @@
       ChangeNotes.Factory changeNotesFactory,
       ProjectCache projectCache,
       ApprovalsUtil approvalsUtil,
-      NotifyUtil notifyUtil) {
+      NotifyResolver notifyResolver) {
     this.seq = seq;
     this.queryProvider = queryProvider;
     this.gitManager = gitManager;
@@ -130,7 +131,7 @@
     this.changeNotesFactory = changeNotesFactory;
     this.projectCache = projectCache;
     this.approvalsUtil = approvalsUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
   }
 
   public Result cherryPick(
@@ -139,8 +140,8 @@
       PatchSet patch,
       CherryPickInput input,
       Branch.NameKey dest)
-      throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
+          RestApiException, ConfigInvalidException, NoSuchProjectException {
     return cherryPick(
         batchUpdateFactory,
         change,
@@ -157,8 +158,8 @@
       ObjectId sourceCommit,
       CherryPickInput input,
       Branch.NameKey dest)
-      throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException,
+          RestApiException, ConfigInvalidException, NoSuchProjectException {
 
     IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
@@ -246,11 +247,12 @@
         }
         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.
@@ -272,7 +274,7 @@
   }
 
   private RevCommit getBaseCommit(Ref destRef, String project, RevWalk revWalk, String base)
-      throws RestApiException, IOException, OrmException {
+      throws RestApiException, IOException {
     RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
     // The tip commit of the destination ref is the default base for the newly created change.
     if (Strings.isNullOrEmpty(base)) {
@@ -303,31 +305,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();
   }
@@ -340,7 +335,7 @@
       @Nullable Change sourceChange,
       ObjectId sourceCommit,
       CherryPickInput input)
-      throws OrmException, IOException, BadRequestException, ConfigInvalidException {
+      throws IOException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
     Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
@@ -350,9 +345,7 @@
         .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(changeNotesFactory.createChecked(sourceChange));
@@ -368,6 +361,12 @@
     return changeId;
   }
 
+  private NotifyResolver.Result resolveNotify(CherryPickInput input)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    return notifyResolver.resolve(
+        firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
+  }
+
   private String messageForDestinationChange(
       PatchSet.Id patchSetId,
       Branch.NameKey sourceBranch,
@@ -382,12 +381,10 @@
     stringBuilder.append(".");
 
     if (!cherryPickCommit.getFilesWithGitConflicts().isEmpty()) {
-      stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
-      cherryPickCommit
-          .getFilesWithGitConflicts()
-          .stream()
+      stringBuilder.append("\n\nThe following files contain Git conflicts:");
+      cherryPickCommit.getFilesWithGitConflicts().stream()
           .sorted()
-          .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
+          .forEach(filePath -> stringBuilder.append("\n* ").append(filePath));
     }
 
     return stringBuilder.toString();
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index f76689c..f34f178 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,8 +72,8 @@
   @Override
   public CherryPickChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
-      throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
+      throws IOException, UpdateException, RestApiException, PermissionBackendException,
+          ConfigInvalidException, NoSuchProjectException {
     RevCommit commit = rsrc.getCommit();
     String message = Strings.nullToEmpty(input.message).trim();
     input.message = message.isEmpty() ? commit.getFullMessage() : message;
diff --git a/java/com/google/gerrit/server/restapi/change/Comments.java b/java/com/google/gerrit/server/restapi/change/Comments.java
index 22f376b..d9a0d57 100644
--- a/java/com/google/gerrit/server/restapi/change/Comments.java
+++ b/java/com/google/gerrit/server/restapi/change/Comments.java
@@ -24,7 +24,6 @@
 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.Singleton;
 
@@ -55,8 +54,7 @@
   }
 
   @Override
-  public CommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException {
+  public CommentResource parse(RevisionResource rev, IdString id) throws ResourceNotFoundException {
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index af54ed5..2326027 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;
@@ -43,18 +45,17 @@
 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;
@@ -70,12 +71,10 @@
 import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
@@ -113,7 +112,7 @@
   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;
 
@@ -134,7 +133,7 @@
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
       MergeUtil.Factory mergeUtilFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.anonymousCowardName = anonymousCowardName;
@@ -152,15 +151,44 @@
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
     this.mergeUtilFactory = mergeUtilFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.contributorAgreements = contributorAgreements;
   }
 
   @Override
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
-      throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
-          UpdateException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
+          PermissionBackendException, ConfigInvalidException {
+    IdentifiedUser me = user.get().asIdentifiedUser();
+    checkAndSanitizeChangeInput(input, me);
+
+    ProjectResource projectResource = projectsCollection.parse(input.project);
+    ProjectState projectState = projectResource.getProjectState();
+    projectState.checkStatePermitsWrite();
+
+    Project.NameKey project = projectResource.getNameKey();
+    contributorAgreements.check(project, user.get());
+
+    checkRequiredPermissions(project, input.branch);
+
+    Change newChange = createNewChange(input, me, projectState, updateFactory);
+    ChangeJson json = jsonFactory.noOptions();
+    return Response.created(json.format(newChange));
+  }
+
+  /**
+   * Checks and sanitizes the user input, e.g. check whether the input is legal; clean the input so
+   * that it meets the requirement for creating a change; set a field based on the global configs,
+   * etc.
+   *
+   * @param input the {@code ChangeInput} from the request. Note this method modify the {@code
+   *     ChangeInput} object so that it can be reused directly by follow-up code.
+   * @param me the user who sent the current request to create a change.
+   * @throws BadRequestException if the input is not legal.
+   */
+  private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
+      throws RestApiException, PermissionBackendException, IOException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -168,165 +196,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, 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).check(ChangePermission.READ);
-        } catch (AuthException e) {
-          throw new UnprocessableEntityException("Read not permitted for " + input.baseChange);
-        }
-        PatchSet ps = psUtil.current(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(project, me, now)) {
+      try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
         bu.setRepository(git, rw, oi);
+        bu.setNotify(
+            notifyResolver.resolve(
+                firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
         bu.insertChange(ins);
         bu.execute();
       }
-      ChangeJson json = jsonFactory.noOptions();
-      return Response.created(json.format(ins.getChange()));
+      return ins.getChange();
     } catch (IllegalArgumentException e) {
       throw new BadRequestException(e.getMessage());
     }
   }
 
+  private ChangeNotes getBaseChange(String baseChange)
+      throws UnprocessableEntityException, PermissionBackendException {
+    List<ChangeNotes> notes = changeFinder.find(baseChange);
+    if (notes.size() != 1) {
+      throw new UnprocessableEntityException("Base change not found: " + baseChange);
+    }
+    ChangeNotes change = Iterables.getOnlyElement(notes);
+    try {
+      permissionBackend.currentUser().change(change).check(ChangePermission.READ);
+    } catch (AuthException e) {
+      throw new UnprocessableEntityException("Read not permitted for " + baseChange);
+    }
+
+    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,
@@ -369,8 +454,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,
@@ -383,8 +467,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;
@@ -393,16 +476,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 fa8adc0..b6e7628 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -72,7 +71,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
     } else if (in.message == null || in.message.trim().isEmpty()) {
@@ -106,7 +105,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, UnprocessableEntityException,
+        throws ResourceNotFoundException, UnprocessableEntityException,
             PatchListNotAvailableException {
       PatchSet ps = psUtil.get(ctx.getNotes(), psId);
       if (ps == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 6efe959..d3e737f 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;
@@ -41,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;
@@ -57,7 +57,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -121,8 +120,7 @@
   @Override
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in)
-      throws OrmException, IOException, RestApiException, UpdateException,
-          PermissionBackendException {
+      throws IOException, RestApiException, UpdateException, PermissionBackendException {
     // Not allowed to create a new patch set if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
@@ -183,11 +181,10 @@
           patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
       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);
         }
@@ -201,7 +198,7 @@
   }
 
   private PatchSet findBasePatchSet(String baseChange)
-      throws PermissionBackendException, OrmException, UnprocessableEntityException {
+      throws PermissionBackendException, UnprocessableEntityException {
     List<ChangeNotes> notes = changeFinder.find(baseChange);
     if (notes.size() != 1) {
       throw new UnprocessableEntityException("Base change not found: " + baseChange);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index 907347f..02387be 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -68,7 +67,7 @@
   @Override
   protected Response<AccountInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     try (BatchUpdate bu =
@@ -88,7 +87,7 @@
     private AccountState deletedAssignee;
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws RestApiException, OrmException {
+    public boolean updateChange(ChangeContext ctx) throws RestApiException {
       change = ctx.getChange();
       ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
       Account.Id currentAssigneeId = change.getAssignee();
@@ -120,7 +119,7 @@
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       assigneeChanged.fire(change, ctx.getAccount(), deletedAssignee, ctx.getWhen());
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index ef19c56..3021d81 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteChangeOp;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -50,7 +51,7 @@
   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().check(ChangePermission.DELETE);
@@ -66,16 +67,16 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
-    Change.Status status = rsrc.getChange().getStatus();
     PermissionBackend.ForChange perm = rsrc.permissions();
     return new UiAction.Description()
         .setLabel("Delete")
         .setTitle("Delete change " + rsrc.getId())
-        .setVisible(and(isChangeDeletable(status), perm.testCond(ChangePermission.DELETE)));
+        .setVisible(and(isChangeDeletable(rsrc), perm.testCond(ChangePermission.DELETE)));
   }
 
-  private static boolean isChangeDeletable(Change.Status status) {
-    if (status == Change.Status.MERGED) {
+  private static boolean isChangeDeletable(ChangeResource rsrc) {
+    Change change = rsrc.getChange();
+    if (change.isMerged()) {
       // Merged changes should never be deleted.
       return false;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
index d49a804..f7f808a 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeEdit.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, IOException, OrmException {
+      throws AuthException, ResourceNotFoundException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (edit.isPresent()) {
       editUtil.delete(edit.get());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index adbe0e6..0fb8e18 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -83,15 +82,14 @@
       BatchUpdate.Factory updateFactory,
       ChangeMessageResource resource,
       DeleteChangeMessageInput input)
-      throws RestApiException, PermissionBackendException, OrmException, UpdateException,
-          IOException {
+      throws RestApiException, PermissionBackendException, UpdateException, IOException {
     CurrentUser user = userProvider.get();
     permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
 
     String newChangeMessage =
         createNewChangeMessage(user.asIdentifiedUser().getName(), input.reason);
     DeleteChangeMessageOp deleteChangeMessageOp =
-        new DeleteChangeMessageOp(resource.getChangeMessageIndex(), newChangeMessage);
+        new DeleteChangeMessageOp(resource.getChangeMessageId(), newChangeMessage);
     try (BatchUpdate batchUpdate =
         updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
       batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
@@ -103,7 +101,7 @@
   }
 
   private ChangeMessageInfo createUpdatedChangeMessageInfo(Change.Id id, int targetIdx)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     List<ChangeMessage> messages = changeMessagesUtil.byChange(notesFactory.createChecked(id));
     ChangeMessage updatedChangeMessage = messages.get(targetIdx);
     AccountLoader accountLoader = accountLoaderFactory.create(true);
@@ -130,18 +128,18 @@
   }
 
   private class DeleteChangeMessageOp implements BatchUpdateOp {
-    private final int targetMessageIdx;
+    private final String targetMessageId;
     private final String newMessage;
 
-    DeleteChangeMessageOp(int targetMessageIdx, String newMessage) {
-      this.targetMessageIdx = targetMessageIdx;
+    DeleteChangeMessageOp(String targetMessageIdx, String newMessage) {
+      this.targetMessageId = targetMessageIdx;
       this.newMessage = newMessage;
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx) {
       PatchSet.Id psId = ctx.getChange().currentPatchSetId();
-      changeMessagesUtil.replaceChangeMessage(ctx.getUpdate(psId), targetMessageIdx, newMessage);
+      changeMessagesUtil.replaceChangeMessage(ctx.getUpdate(psId), targetMessageId, newMessage);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index e73c4f3..30a8efd 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -74,8 +73,8 @@
   @Override
   public CommentInfo applyImpl(
       BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input)
-      throws RestApiException, IOException, ConfigInvalidException, OrmException,
-          PermissionBackendException, UpdateException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
+          UpdateException {
     CurrentUser user = userProvider.get();
     permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index bcff6e4..de04d36 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collections;
@@ -83,7 +82,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
+        throws ResourceNotFoundException, PatchListNotAvailableException {
       Optional<Comment> maybeComment =
           commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 571c319..8601e68 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -16,13 +16,14 @@
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -36,25 +37,22 @@
 @Singleton
 public class DeletePrivate
     extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> {
-  private final ChangeMessagesUtil cmUtil;
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
 
   @Inject
   DeletePrivate(
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory) {
     super(retryHelper);
-    this.cmUtil = cmUtil;
     this.permissionBackend = permissionBackend;
     this.setPrivateOpFactory = setPrivateOpFactory;
   }
 
   @Override
   protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, @Nullable SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
     if (!canDeletePrivate(rsrc).value()) {
       throw new AuthException("not allowed to unmark private");
@@ -64,7 +62,7 @@
       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(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 6404256..c86d0ca 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
@@ -17,8 +17,8 @@
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
 import com.google.gerrit.extensions.webui.UiAction;
-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;
@@ -29,10 +29,9 @@
   @Inject
   DeletePrivateByPost(
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory) {
-    super(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 da1679c..12dbcdd 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -15,8 +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.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;
@@ -57,9 +62,10 @@
             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);
       }
@@ -68,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 60d1163..77894fb 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
@@ -36,7 +37,7 @@
 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;
@@ -55,12 +56,12 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
@@ -72,7 +73,7 @@
   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;
 
@@ -85,7 +86,7 @@
       IdentifiedUser.GenericFactory userFactory,
       VoteDeleted voteDeleted,
       DeleteVoteSender.Factory deleteVoteSenderFactory,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache) {
     super(retryHelper);
@@ -95,7 +96,7 @@
     this.userFactory = userFactory;
     this.voteDeleted = voteDeleted;
     this.deleteVoteSenderFactory = deleteVoteSenderFactory;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
   }
@@ -103,7 +104,7 @@
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
-      throws RestApiException, UpdateException, IOException {
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new DeleteVoteInput();
     }
@@ -123,6 +124,9 @@
     try (BatchUpdate bu =
         updateFactory.create(
             change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+      bu.setNotify(
+          notifyResolver.resolve(
+              firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
       bu.addOp(
           change.getId(),
           new Op(
@@ -158,8 +162,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, AuthException, ResourceNotFoundException, IOException,
-            PermissionBackendException {
+        throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException {
       change = ctx.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
       ps = psUtil.current(ctx.getNotes());
@@ -217,17 +220,17 @@
       }
 
       IdentifiedUser user = ctx.getIdentifiedUser();
-      if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
-        try {
+      try {
+        NotifyResolver.Result notify = ctx.getNotify(change.getId());
+        if (notify.shouldNotify()) {
           ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
           cm.setFrom(user.getAccountId());
           cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
-          cm.setNotify(input.notify);
-          cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+          cm.setNotify(notify);
           cm.send();
-        } catch (Exception e) {
-          logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
         }
+      } catch (Exception e) {
+        logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
       }
 
       voteDeleted.fire(
diff --git a/java/com/google/gerrit/server/restapi/change/DownloadContent.java b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
index b6564c0..1022cad 100644
--- a/java/com/google/gerrit/server/restapi/change/DownloadContent.java
+++ b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -43,7 +42,7 @@
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException, NoSuchChangeException, OrmException {
+      throws ResourceNotFoundException, IOException, NoSuchChangeException {
     String path = rsrc.getPatchKey().get();
     RevisionResource rev = rsrc.getRevision();
     ObjectId revstr = ObjectId.fromString(rev.getPatchSet().getRevision().get());
diff --git a/java/com/google/gerrit/server/restapi/change/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index 9f06252..dd61ca0 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -62,7 +61,7 @@
 
   @Override
   public DraftCommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException {
+      throws ResourceNotFoundException, AuthException {
     checkIdentifiedUser();
     String uuid = id.get();
     for (Comment c :
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index 5f2c370..2a0cd58 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -49,7 +49,6 @@
 import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -146,7 +145,7 @@
 
     @Override
     public Response<?> apply(RevisionResource resource)
-        throws AuthException, BadRequestException, ResourceNotFoundException, OrmException,
+        throws AuthException, BadRequestException, ResourceNotFoundException,
             RepositoryNotFoundException, IOException, PatchListNotAvailableException,
             PermissionBackendException {
       checkOptions();
@@ -223,8 +222,7 @@
       }
     }
 
-    private Collection<String> reviewed(RevisionResource resource)
-        throws AuthException, OrmException {
+    private Collection<String> reviewed(RevisionResource resource) throws AuthException {
       CurrentUser user = self.get();
       if (!(user.isIdentifiedUser())) {
         throw new AuthException("Authentication required");
@@ -233,9 +231,7 @@
       Account.Id userId = user.getAccountId();
       PatchSet patchSetId = resource.getPatchSet();
       Optional<PatchSetWithReviewedFiles> o;
-      o =
-          accountPatchReviewStore.call(
-              s -> s.findReviewed(patchSetId.getId(), userId), OrmException.class);
+      o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.getId(), userId));
 
       if (o.isPresent()) {
         PatchSetWithReviewedFiles res = o.get();
@@ -257,7 +253,7 @@
 
     private List<String> copy(
         Set<String> paths, PatchSet.Id old, RevisionResource resource, Account.Id userId)
-        throws IOException, PatchListNotAvailableException, OrmException {
+        throws IOException, PatchListNotAvailableException {
       Project.NameKey project = resource.getChange().getProject();
       try (Repository git = gitManager.openRepository(project);
           ObjectReader reader = git.newObjectReader();
@@ -318,8 +314,7 @@
         }
 
         accountPatchReviewStore.run(
-            s -> s.markReviewed(resource.getPatchSet().getId(), userId, pathList),
-            OrmException.class);
+            s -> s.markReviewed(resource.getPatchSet().getId(), userId, pathList));
         return pathList;
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 1d8726d..855d1f4 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.change.FixResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -50,7 +49,7 @@
 
   @Override
   public FixResource parse(RevisionResource revisionResource, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException {
     String fixId = id.get();
     ChangeNotes changeNotes = revisionResource.getNotes();
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetAssignee.java b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
index e95f8d8..f89fe1b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Optional;
@@ -36,8 +35,7 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public Response<AccountInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
     Optional<Account.Id> assignee = Optional.ofNullable(rsrc.getChange().getAssignee());
     if (assignee.isPresent()) {
       return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.get()));
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index c7a8015..bb92f00 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gitiles.blame.cache.BlameCache;
 import com.google.gitiles.blame.cache.Region;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -79,7 +78,7 @@
 
   @Override
   public Response<List<BlameInfo>> apply(FileResource resource)
-      throws RestApiException, OrmException, IOException, InvalidChangeOperationException {
+      throws RestApiException, IOException, InvalidChangeOperationException {
     Project.NameKey project = resource.getRevision().getChange().getProject();
     try (Repository repository = repoManager.openRepository(project);
         ObjectInserter ins = repository.newObjectInserter();
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index a8f8bbb..c28741b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -14,43 +14,79 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
 import org.kohsuke.args4j.Option;
 
-public class GetChange implements RestReadView<ChangeResource> {
+public class GetChange
+    implements RestReadView<ChangeResource>,
+        DynamicOptions.BeanReceiver,
+        DynamicOptions.BeanProvider {
   private final ChangeJson.Factory json;
+  private final DynamicSet<ChangeAttributeFactory> attrFactories;
   private final EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
+  private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
 
   @Option(name = "-o", usage = "Output options")
-  void addOption(ListChangesOption o) {
+  public void addOption(ListChangesOption o) {
     options.add(o);
   }
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Inject
-  GetChange(ChangeJson.Factory json) {
+  GetChange(ChangeJson.Factory json, DynamicSet<ChangeAttributeFactory> attrFactories) {
     this.json = json;
+    this.attrFactories = attrFactories;
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(rsrc));
+  public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
+    dynamicBeans.put(plugin, dynamicBean);
   }
 
-  Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.create(options).format(rsrc));
+  @Override
+  public DynamicBean getDynamicBean(String plugin) {
+    return dynamicBeans.get(plugin);
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc) {
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
+  }
+
+  Response<ChangeInfo> apply(RevisionResource rsrc) {
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
+  }
+
+  private ChangeJson newChangeJson() {
+    return json.create(options, this::buildPluginInfo);
+  }
+
+  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
+    return PluginDefinedAttributesFactories.createAll(
+        cd, this, Streams.stream(attrFactories.entries()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetComment.java b/java/com/google/gerrit/server/restapi/change/GetComment.java
index d067dff..0109c95 100644
--- a/java/com/google/gerrit/server/restapi/change/GetComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,7 +33,7 @@
   }
 
   @Override
-  public CommentInfo apply(CommentResource rsrc) throws OrmException, PermissionBackendException {
+  public CommentInfo apply(CommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetContent.java b/java/com/google/gerrit/server/restapi/change/GetContent.java
index c133581..1d35ab5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetContent.java
+++ b/java/com/google/gerrit/server/restapi/change/GetContent.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.patch.Text;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -63,7 +62,7 @@
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException, BadRequestException, OrmException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     String path = rsrc.getPatchKey().get();
     if (Patch.COMMIT_MSG.equals(path)) {
       String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes());
@@ -83,7 +82,7 @@
         parent);
   }
 
-  private String getMessage(ChangeNotes notes) throws OrmException, IOException {
+  private String getMessage(ChangeNotes notes) throws IOException {
     Change.Id changeId = notes.getChangeId();
     PatchSet ps = psUtil.current(notes);
     if (ps == null) {
@@ -99,7 +98,7 @@
     }
   }
 
-  private byte[] getMergeList(ChangeNotes notes) throws OrmException, IOException {
+  private byte[] getMergeList(ChangeNotes notes) throws IOException {
     Change.Id changeId = notes.getChangeId();
     PatchSet ps = psUtil.current(notes);
     if (ps == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetDetail.java b/java/com/google/gerrit/server/restapi/change/GetDetail.java
index ab75ab7..e31d84b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDetail.java
@@ -18,12 +18,13 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import org.kohsuke.args4j.Option;
 
-public class GetDetail implements RestReadView<ChangeResource> {
+public class GetDetail implements RestReadView<ChangeResource>, DynamicOptions.BeanReceiver {
   private final GetChange delegate;
 
   @Option(name = "-o", usage = "Output options")
@@ -47,7 +48,17 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
+  public void setDynamicBean(String plugin, DynamicBean dynamicBean) {
+    delegate.setDynamicBean(plugin, dynamicBean);
+  }
+
+  @Override
+  public Class<? extends DynamicOptions.BeanReceiver> getExportedBeanReceiver() {
+    return delegate.getExportedBeanReceiver();
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc) {
     return delegate.apply(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 1fae739..761500a 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -56,7 +56,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.List;
@@ -126,7 +125,7 @@
 
   @Override
   public Response<DiffInfo> apply(FileResource resource)
-      throws ResourceConflictException, ResourceNotFoundException, OrmException, AuthException,
+      throws ResourceConflictException, ResourceNotFoundException, AuthException,
           InvalidChangeOperationException, IOException, PermissionBackendException {
     DiffPreferencesInfo prefs = new DiffPreferencesInfo();
     if (whitespace != null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
index 6049607..ca5b56f 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public CommentInfo apply(DraftCommentResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public CommentInfo apply(DraftCommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetHashtags.java b/java/com/google/gerrit/server/restapi/change/GetHashtags.java
index 8369acf..aff3a44 100644
--- a/java/com/google/gerrit/server/restapi/change/GetHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/GetHashtags.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collections;
@@ -30,7 +29,7 @@
 public class GetHashtags implements RestReadView<ChangeResource> {
   @Override
   public Response<Set<String>> apply(ChangeResource req)
-      throws AuthException, OrmException, IOException, BadRequestException {
+      throws AuthException, IOException, BadRequestException {
     ChangeNotes notes = req.getNotes().load();
     Set<String> hashtags = notes.getHashtags();
     if (hashtags == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
index 279cfe3..1d56669 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collections;
@@ -40,8 +39,7 @@
   }
 
   @Override
-  public Response<List<AccountInfo>> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public Response<List<AccountInfo>> apply(ChangeResource rsrc) throws PermissionBackendException {
 
     Set<Account.Id> pastAssignees = rsrc.getNotes().load().getPastAssignees();
     if (pastAssignees == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
index 75019af..fa5cc36 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.PureRevert;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Optional;
 import org.kohsuke.args4j.Option;
 
 public class GetPureRevert implements RestReadView<ChangeResource> {
@@ -47,8 +47,8 @@
 
   @Override
   public PureRevertInfo apply(ChangeResource rsrc)
-      throws ResourceConflictException, IOException, BadRequestException, OrmException,
-          AuthException {
-    return pureRevert.get(rsrc.getNotes(), claimedOriginal);
+      throws ResourceConflictException, IOException, BadRequestException, AuthException {
+    boolean isPureRevert = pureRevert.get(rsrc.getNotes(), Optional.ofNullable(claimedOriginal));
+    return new PureRevertInfo(isPureRevert);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 9a65165..332cc4d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -27,6 +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.server.ChangeUtil;
 import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -35,7 +36,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -43,6 +43,7 @@
 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;
@@ -68,7 +69,7 @@
 
   @Override
   public RelatedChangesInfo apply(RevisionResource rsrc)
-      throws RepositoryNotFoundException, IOException, OrmException, NoSuchProjectException,
+      throws RepositoryNotFoundException, IOException, NoSuchProjectException,
           PermissionBackendException {
     RelatedChangesInfo relatedChangesInfo = new RelatedChangesInfo();
     relatedChangesInfo.changes = getRelated(rsrc);
@@ -76,7 +77,7 @@
   }
 
   private List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
-      throws OrmException, IOException, PermissionBackendException {
+      throws IOException, PermissionBackendException {
     Set<String> groups = getAllGroups(rsrc.getNotes(), psUtil);
     if (groups.isEmpty()) {
       return Collections.emptyList();
@@ -121,12 +122,11 @@
   }
 
   @VisibleForTesting
-  public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil)
-      throws OrmException {
+  public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil) {
     return psUtil.byChange(notes).stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet());
   }
 
-  private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) throws OrmException {
+  private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) {
     for (ChangeData cd : cds) {
       if (cd.getId().equals(wantedPs.getId().getParentKey())) {
         if (cd.patchSet(wantedPs.getId()) == null) {
@@ -147,7 +147,7 @@
       info._revisionNumber = ps != null ? ps.getPatchSetId() : null;
       PatchSet.Id curr = change.currentPatchSetId();
       info._currentRevisionNumber = curr != null ? curr.get() : null;
-      info.status = change.getStatus().asChangeStatus().toString();
+      info.status = ChangeUtil.status(change).toUpperCase(Locale.US);
     }
 
     info.commit = new CommitInfo();
diff --git a/java/com/google/gerrit/server/restapi/change/GetReview.java b/java/com/google/gerrit/server/restapi/change/GetReview.java
index 40e132d..8d941ab 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReview.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReview.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -35,7 +34,7 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
+  public Response<ChangeInfo> apply(RevisionResource rsrc) {
     return delegate.apply(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetReviewer.java b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
index a11380b..73760da 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ReviewerResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public List<ReviewerInfo> apply(ReviewerResource rsrc) throws PermissionBackendException {
     return json.format(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
index b0b49a3..c4da3b6 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
@@ -16,6 +16,7 @@
 
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.Response;
@@ -28,8 +29,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,7 +56,7 @@
   }
 
   @Override
-  public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) throws OrmException {
+  public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) {
     return Response.withMustRevalidate(delegate.format(rsrc));
   }
 
@@ -73,8 +72,8 @@
         changeResourceFactory.create(cd.notes(), user).prepareETag(h, user);
       }
       h.putBoolean(cs.furtherHiddenChanges());
-    } catch (IOException | OrmException | PermissionBackendException e) {
-      throw new OrmRuntimeException(e);
+    } catch (IOException | PermissionBackendException e) {
+      throw new StorageException(e);
     }
     return h.hash().toString();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
index 0197068..75d994d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,8 +33,7 @@
   }
 
   @Override
-  public RobotCommentInfo apply(RobotCommentResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public RobotCommentInfo apply(RobotCommentResource rsrc) throws PermissionBackendException {
     return commentJson.get().newRobotCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Ignore.java b/java/com/google/gerrit/server/restapi/change/Ignore.java
index e319451..25cf311 100644
--- a/java/com/google/gerrit/server/restapi/change/Ignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Ignore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -26,7 +27,6 @@
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.StarredChangesUtil.MutuallyExclusiveLabelsException;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -51,7 +51,7 @@
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
+      throws RestApiException, IllegalLabelException {
     try {
       if (rsrc.isUserOwner()) {
         throw new BadRequestException("cannot ignore own change");
@@ -73,7 +73,7 @@
   private boolean isIgnored(ChangeResource rsrc) {
     try {
       return stars.isIgnored(rsrc);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index f04a3e8..90dad98 100644
--- a/java/com/google/gerrit/server/restapi/change/Index.java
+++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -45,7 +44,7 @@
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
-      throws IOException, AuthException, OrmException, PermissionBackendException {
+      throws IOException, AuthException, PermissionBackendException {
     permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
     indexer.index(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 902a67e..992f602 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,7 +45,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
+      throws AuthException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index 7bdc139..1939385 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -47,7 +46,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
+      throws AuthException, PermissionBackendException {
     if (!rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
index ba09281..a2e3d4b 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.List;
@@ -42,12 +41,10 @@
   }
 
   @Override
-  public List<ChangeMessageInfo> apply(ChangeResource resource)
-      throws OrmException, PermissionBackendException {
+  public List<ChangeMessageInfo> apply(ChangeResource resource) throws PermissionBackendException {
     List<ChangeMessage> messages = changeMessagesUtil.byChange(resource.getNotes());
     List<ChangeMessageInfo> messageInfos =
-        messages
-            .stream()
+        messages.stream()
             .map(m -> createChangeMessageInfo(m, accountLoader))
             .collect(Collectors.toList());
     accountLoader.fill();
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index 3ea2ac2..e5840fd 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.List;
@@ -44,7 +43,7 @@
 
   @Override
   public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException, PermissionBackendException {
+      throws AuthException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 725a89b..12732ff 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.LinkedHashMap;
@@ -31,7 +30,7 @@
 import java.util.Map;
 
 @Singleton
-class ListReviewers implements RestReadView<ChangeResource> {
+public class ListReviewers implements RestReadView<ChangeResource> {
   private final ApprovalsUtil approvalsUtil;
   private final ReviewerJson json;
   private final ReviewerResource.Factory resourceFactory;
@@ -45,8 +44,7 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ChangeResource rsrc)
-      throws OrmException, PermissionBackendException {
+  public List<ReviewerInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
     Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
     for (Account.Id accountId : approvalsUtil.getReviewers(rsrc.getNotes()).all()) {
       if (!reviewers.containsKey(accountId.toString())) {
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
index f10d92b..b39ba63 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -36,7 +35,7 @@
   }
 
   @Override
-  protected Iterable<Comment> listComments(RevisionResource rsrc) throws OrmException {
+  protected Iterable<Comment> listComments(RevisionResource rsrc) {
     ChangeNotes notes = rsrc.getNotes();
     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 3df7e9c..a46bd6c 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,7 +38,7 @@
     this.commentsUtil = commentsUtil;
   }
 
-  protected Iterable<Comment> listComments(RevisionResource rsrc) throws OrmException {
+  protected Iterable<Comment> listComments(RevisionResource rsrc) {
     return commentsUtil.draftByPatchSetAuthor(
         rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes());
   }
@@ -50,7 +49,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
@@ -59,7 +58,7 @@
   }
 
   public ImmutableList<CommentInfo> getComments(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index 6e7ffd9..920cde9 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.LinkedHashMap;
@@ -47,7 +46,7 @@
 
   @Override
   public List<ReviewerInfo> apply(RevisionResource rsrc)
-      throws OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws MethodNotAllowedException, PermissionBackendException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index c13b1e7..bbf46a3 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,7 +40,7 @@
 
   @Override
   public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
@@ -50,7 +49,7 @@
   }
 
   public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
@@ -58,7 +57,7 @@
         .formatAsList(listComments(rsrc));
   }
 
-  private Iterable<RobotComment> listComments(RevisionResource rsrc) throws OrmException {
+  private Iterable<RobotComment> listComments(RevisionResource rsrc) {
     return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().getId());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
index 49c8fb6..4c942d2 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -24,7 +25,6 @@
 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.Singleton;
 
@@ -52,7 +52,7 @@
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
+      throws RestApiException, IllegalLabelException {
     stars.markAsReviewed(rsrc);
     return Response.ok("");
   }
@@ -62,7 +62,7 @@
       return changeDataFactory
           .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
index 0651e7b..5945b14 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -23,7 +24,6 @@
 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.Singleton;
 
@@ -50,8 +50,7 @@
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws OrmException, IllegalLabelException {
+  public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException {
     stars.markAsUnreviewed(rsrc);
     return Response.ok("");
   }
@@ -61,7 +60,7 @@
       return changeDataFactory
           .create(rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index f8877374..ece8938 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,7 +37,6 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -90,13 +90,12 @@
 
   @Override
   public MergeableInfo apply(RevisionResource resource)
-      throws AuthException, ResourceConflictException, BadRequestException, OrmException,
-          IOException {
+      throws AuthException, ResourceConflictException, BadRequestException, IOException {
     Change change = resource.getChange();
     PatchSet ps = resource.getPatchSet();
     MergeableInfo result = new MergeableInfo();
 
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     } else if (!ps.getId().equals(change.currentPatchSetId())) {
       // Only the current revision is mergeable. Others always fail.
@@ -136,10 +135,10 @@
     return result;
   }
 
-  private SubmitType getSubmitType(ChangeData cd) throws OrmException {
+  private SubmitType getSubmitType(ChangeData cd) {
     SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd);
     if (rec.status != SubmitTypeRecord.Status.OK) {
-      throw new OrmException("Submit type rule failed: " + rec);
+      throw new StorageException("Submit type rule failed: " + rec);
     }
     return rec.type;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index c09d30a..a57bd64 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -32,12 +32,16 @@
 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;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.SetAssigneeOp;
 import com.google.gerrit.server.change.SetHashtagsOp;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.restapi.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.restapi.change.Reviewed.PutReviewed;
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 2932e5c..f2335b1 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -23,6 +23,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.MoveInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -33,7 +34,6 @@
 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;
@@ -62,7 +62,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -117,8 +116,7 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
-      throws RestApiException, OrmException, UpdateException, PermissionBackendException,
-          IOException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException {
     if (!moveEnabled) {
       // This will be removed with the above config once we reach consensus for the move change
       // behavior. See: https://bugs.chromium.org/p/gerrit/issues/detail?id=9877
@@ -133,7 +131,7 @@
     }
     input.destinationBranch = RefNames.fullName(input.destinationBranch);
 
-    if (change.getStatus().isClosed()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     }
 
@@ -178,10 +176,9 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws OrmException, ResourceConflictException, IOException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
       change = ctx.getChange();
-      if (change.getStatus() != Status.NEW) {
+      if (!change.isNew()) {
         throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
       }
 
@@ -259,7 +256,7 @@
      */
     private void updateApprovals(
         ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project)
-        throws IOException, OrmException {
+        throws IOException {
       List<PatchSetApproval> approvals = new ArrayList<>();
       for (PatchSetApproval psa :
           approvalsUtil.byPatchSet(
@@ -293,7 +290,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       return description;
     }
 
@@ -311,7 +308,7 @@
       if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index b1a250c..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,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -42,7 +42,6 @@
 public class PostPrivate
     extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>>
     implements UiAction<ChangeResource> {
-  private final ChangeMessagesUtil cmUtil;
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
   private final boolean disablePrivateChanges;
@@ -50,12 +49,10 @@
   @Inject
   PostPrivate(
       RetryHelper retryHelper,
-      ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
       SetPrivateOp.Factory setPrivateOpFactory,
       @GerritServerConfig Config config) {
     super(retryHelper);
-    this.cmUtil = cmUtil;
     this.permissionBackend = permissionBackend;
     this.setPrivateOpFactory = setPrivateOpFactory;
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
@@ -77,7 +74,7 @@
       return Response.ok("");
     }
 
-    SetPrivateOp op = setPrivateOpFactory.create(cmUtil, true, input);
+    SetPrivateOp op = setPrivateOpFactory.create(true, input);
     try (BatchUpdate u =
         updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getId(), op).execute();
@@ -92,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 d2913ef..9a843cd 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;
@@ -90,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;
@@ -121,7 +119,6 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -169,7 +166,7 @@
   private final CommentAdded commentAdded;
   private final ReviewerAdder reviewerAdder;
   private final AddReviewersEmail addReviewersEmail;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final Config gerritConfig;
   private final WorkInProgressOp.Factory workInProgressOpFactory;
   private final ProjectCache projectCache;
@@ -192,7 +189,7 @@
       CommentAdded commentAdded,
       ReviewerAdder reviewerAdder,
       AddReviewersEmail addReviewersEmail,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       @GerritServerConfig Config gerritConfig,
       WorkInProgressOp.Factory workInProgressOpFactory,
       ProjectCache projectCache,
@@ -211,7 +208,7 @@
     this.commentAdded = commentAdded;
     this.reviewerAdder = reviewerAdder;
     this.addReviewersEmail = addReviewersEmail;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.gerritConfig = gerritConfig;
     this.workInProgressOpFactory = workInProgressOpFactory;
     this.projectCache = projectCache;
@@ -222,15 +219,15 @@
   @Override
   protected Response<ReviewResult> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException, PatchListNotAvailableException {
     return apply(updateFactory, revision, input, TimeUtil.nowTs());
   }
 
   public Response<ReviewResult> apply(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException, PatchListNotAvailableException {
     // Respect timestamp, but truncate at change created-on time.
     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     if (revision.getEdit().isPresent()) {
@@ -253,14 +250,10 @@
       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;
@@ -268,12 +261,6 @@
     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(revision.getNotes(), revision.getUser(), reviewerInput, true);
         reviewerJsonResults.put(reviewerInput.reviewer, result.result);
@@ -316,6 +303,7 @@
       // updated set of reviewers. Also keep track of whether the user added
       // themselves as a reviewer or to the CC list.
       for (ReviewerAddition reviewerResult : reviewerResults) {
+        reviewerResult.op.suppressEmail(); // Send a single batch email below.
         bu.addOp(revision.getChange().getId(), reviewerResult.op);
         if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
           for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
@@ -340,6 +328,7 @@
         // isn't being explicitly added, and isn't voting on any label.
         // Automatically CC them on this change so they receive replies.
         ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision);
+        selfAddition.op.suppressEmail();
         bu.addOp(revision.getChange().getId(), selfAddition.op);
       }
 
@@ -350,26 +339,30 @@
           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();
 
@@ -379,21 +372,24 @@
         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) {
@@ -409,11 +405,12 @@
     }
 
     if (workInProgress && !c.hasReviewStarted()) {
-      // If review hasn't started we want to minimize recipients, no matter who
-      // the author is.
-      return NotifyHandling.OWNER;
+      // If review hasn't started we want to eliminate notifications, no matter who the author is.
+      return NotifyHandling.NONE;
     }
 
+    // Otherwise, it's either a non-WIP change, or a WIP change where review has started. Notify
+    // everyone.
     return NotifyHandling.ALL;
   }
 
@@ -421,9 +418,7 @@
       CurrentUser user,
       Change change,
       List<ReviewerAddition> reviewerAdditions,
-      @Nullable NotifyHandling notify,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
-      boolean readyForReview) {
+      NotifyResolver.Result notify) {
     List<Account.Id> to = new ArrayList<>();
     List<Account.Id> cc = new ArrayList<>();
     List<Address> toByEmail = new ArrayList<>();
@@ -438,19 +433,11 @@
       }
     }
     addReviewersEmail.emailReviewers(
-        user.asIdentifiedUser(),
-        change,
-        to,
-        cc,
-        toByEmail,
-        ccByEmail,
-        notify,
-        accountsToNotify,
-        readyForReview);
+        user.asIdentifiedUser(), change, to, cc, toByEmail, ccByEmail, notify);
   }
 
   private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
-      throws BadRequestException, AuthException, UnprocessableEntityException, OrmException,
+      throws BadRequestException, AuthException, UnprocessableEntityException,
           PermissionBackendException, IOException, ConfigInvalidException {
     if (in.labels == null || in.labels.isEmpty()) {
       throw new AuthException(
@@ -491,7 +478,7 @@
           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).change(rev.getNotes()).check(ChangePermission.READ);
     } catch (AuthException e) {
@@ -787,8 +774,7 @@
   private static void ensureRangesDoNotOverlap(
       String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
     List<Range> sortedRanges =
-        fixReplacementInfos
-            .stream()
+        fixReplacementInfos.stream()
             .map(fixReplacementInfo -> fixReplacementInfo.range)
             .sorted()
             .collect(toList());
@@ -849,7 +835,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;
@@ -860,20 +845,15 @@
     private Map<String, Short> approvals = new HashMap<>();
     private Map<String, Short> oldApprovals = new HashMap<>();
 
-    private Op(
-        ProjectState projectState,
-        PatchSet.Id psId,
-        ReviewInput in,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+    private Op(ProjectState projectState, PatchSet.Id psId, ReviewInput in) {
       this.projectState = projectState;
       this.psId = psId;
       this.in = in;
-      this.accountsToNotify = requireNonNull(accountsToNotify);
     }
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException,
+        throws ResourceConflictException, UnprocessableEntityException, IOException,
             PatchListNotAvailableException {
       user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
@@ -887,22 +867,14 @@
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       if (message == null) {
         return;
       }
-      if (in.notify.compareTo(NotifyHandling.NONE) > 0 || !accountsToNotify.isEmpty()) {
+      NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
+      if (notify.shouldNotify()) {
         email
-            .create(
-                in.notify,
-                accountsToNotify,
-                notes,
-                ps,
-                user,
-                message,
-                comments,
-                in.message,
-                labelDelta)
+            .create(notify, notes, ps, user, message, comments, in.message, labelDelta)
             .sendAsync();
       }
       commentAdded.fire(
@@ -916,7 +888,7 @@
     }
 
     private boolean insertComments(ChangeContext ctx)
-        throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
+        throws UnprocessableEntityException, PatchListNotAvailableException {
       Map<String, List<CommentInput>> map = in.comments;
       if (map == null) {
         map = Collections.emptyMap();
@@ -976,8 +948,7 @@
       return !toPublish.isEmpty();
     }
 
-    private boolean insertRobotComments(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException {
+    private boolean insertRobotComments(ChangeContext ctx) throws PatchListNotAvailableException {
       if (in.robotComments == null) {
         return false;
       }
@@ -989,7 +960,7 @@
     }
 
     private List<RobotComment> getNewRobotComments(ChangeContext ctx)
-        throws OrmException, PatchListNotAvailableException {
+        throws PatchListNotAvailableException {
       List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
 
       Set<CommentSetEntry> existingIds =
@@ -1058,23 +1029,19 @@
       return new FixReplacement(fixReplacementInfo.path, range, fixReplacementInfo.replacement);
     }
 
-    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) throws OrmException {
-      return commentsUtil
-          .publishedByChange(ctx.getNotes())
-          .stream()
+    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) {
+      return commentsUtil.publishedByChange(ctx.getNotes()).stream()
           .map(CommentSetEntry::create)
           .collect(toSet());
     }
 
-    private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx) throws OrmException {
-      return commentsUtil
-          .robotCommentsByChange(ctx.getNotes())
-          .stream()
+    private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx) {
+      return commentsUtil.robotCommentsByChange(ctx.getNotes()).stream()
           .map(CommentSetEntry::create)
           .collect(toSet());
     }
 
-    private Map<String, Comment> changeDrafts(ChangeContext ctx) throws OrmException {
+    private Map<String, Comment> changeDrafts(ChangeContext ctx) {
       Map<String, Comment> drafts = new HashMap<>();
       for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), user.getAccountId())) {
         c.tag = in.tag;
@@ -1083,7 +1050,7 @@
       return drafts;
     }
 
-    private Map<String, Comment> patchSetDrafts(ChangeContext ctx) throws OrmException {
+    private Map<String, Comment> patchSetDrafts(ChangeContext ctx) {
       Map<String, Comment> drafts = new HashMap<>();
       for (Comment c :
           commentsUtil.draftByPatchSetAuthor(psId, user.getAccountId(), ctx.getNotes())) {
@@ -1131,7 +1098,7 @@
       return previous;
     }
 
-    private boolean isReviewer(ChangeContext ctx) throws OrmException {
+    private boolean isReviewer(ChangeContext ctx) {
       if (ctx.getAccountId().equals(ctx.getChange().getOwner())) {
         return true;
       }
@@ -1144,13 +1111,13 @@
     }
 
     private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
-        throws OrmException, ResourceConflictException, IOException {
+        throws ResourceConflictException, IOException {
       Map<String, Short> inLabels = firstNonNull(in.labels, Collections.emptyMap());
 
       // If no labels were modified and change is closed, abort early.
       // This avoids trying to record a modified label caused by a user
       // losing access to a label after the change was submitted.
-      if (inLabels.isEmpty() && ctx.getChange().getStatus().isClosed()) {
+      if (inLabels.isEmpty() && ctx.getChange().isClosed()) {
         return false;
       }
 
@@ -1229,11 +1196,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");
       }
 
@@ -1281,8 +1248,7 @@
       if (!reduced.isEmpty()) {
         throw new ResourceConflictException(
             "Cannot reduce vote on labels for closed change: "
-                + reduced
-                    .stream()
+                + reduced.stream()
                     .map(PatchSetApproval::getLabel)
                     .distinct()
                     .sorted()
@@ -1329,7 +1295,7 @@
 
     private Map<String, PatchSetApproval> scanLabels(
         ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del)
-        throws OrmException, IOException {
+        throws IOException {
       LabelTypes labelTypes = projectState.getLabelTypes(ctx.getNotes());
       Map<String, PatchSetApproval> current = new HashMap<>();
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index fdfefab..8abd964 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -16,10 +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.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;
@@ -30,7 +32,6 @@
 import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -42,21 +43,26 @@
         ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
 
   private final ChangeData.Factory changeDataFactory;
+  private final NotifyResolver notifyResolver;
   private final ReviewerAdder reviewerAdder;
 
   @Inject
   PostReviewers(
-      ChangeData.Factory changeDataFactory, RetryHelper retryHelper, ReviewerAdder reviewerAdder) {
+      ChangeData.Factory changeDataFactory,
+      RetryHelper retryHelper,
+      NotifyResolver notifyResolver,
+      ReviewerAdder reviewerAdder) {
     super(retryHelper);
     this.changeDataFactory = changeDataFactory;
+    this.notifyResolver = notifyResolver;
     this.reviewerAdder = reviewerAdder;
   }
 
   @Override
   protected AddReviewerResult applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
-      throws IOException, OrmException, RestApiException, UpdateException,
-          PermissionBackendException, ConfigInvalidException {
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          ConfigInvalidException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -67,6 +73,7 @@
     }
     try (BatchUpdate bu =
         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();
@@ -76,4 +83,14 @@
     addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId()));
     return addition.result;
   }
+
+  private NotifyResolver.Result resolveNotify(ChangeResource rsrc, AddReviewerInput input)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    NotifyHandling notifyHandling = input.notify;
+    if (notifyHandling == null) {
+      notifyHandling =
+          rsrc.getChange().isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+    }
+    return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index 794cf6c..7137b6e 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.submit.MergeOpRepoManager;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -82,7 +81,7 @@
 
   @Override
   public BinaryResult apply(RevisionResource rsrc)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
@@ -99,7 +98,7 @@
     }
 
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
     }
     if (!rsrc.getUser().isIdentifiedUser()) {
@@ -110,7 +109,7 @@
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
@@ -124,8 +123,7 @@
           .setContentType(f.getMimeType())
           .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
       return bin;
-    } catch (OrmException
-        | RestApiException
+    } catch (RestApiException
         | UpdateException
         | IOException
         | ConfigInvalidException
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index 3d401c4..a47037c 100644
--- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -28,7 +31,6 @@
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -39,25 +41,25 @@
 public class PublishChangeEdit
     extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Response<?>> {
   private final ChangeEditUtil editUtil;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
   private final ContributorAgreementsChecker contributorAgreementsChecker;
 
   @Inject
   PublishChangeEdit(
       RetryHelper retryHelper,
       ChangeEditUtil editUtil,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ContributorAgreementsChecker contributorAgreementsChecker) {
     super(retryHelper);
     this.editUtil = editUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.contributorAgreementsChecker = contributorAgreementsChecker;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
-      throws IOException, OrmException, RestApiException, UpdateException, ConfigInvalidException,
+      throws IOException, RestApiException, UpdateException, ConfigInvalidException,
           NoSuchProjectException {
     contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser());
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
@@ -73,8 +75,7 @@
         rsrc.getNotes(),
         rsrc.getUser(),
         edit.get(),
-        in.notify,
-        notifyUtil.resolveAccounts(in.notifyDetails));
+        notifyResolver.resolve(firstNonNull(in.notify, NotifyHandling.ALL), in.notifyDetails));
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index 982b3e6..de62725 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -23,7 +23,6 @@
 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.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
@@ -40,7 +39,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -75,8 +73,8 @@
   @Override
   protected AccountInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
-      throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException, ConfigInvalidException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException,
+          ConfigInvalidException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     input.assignee = Strings.nullToEmpty(input.assignee).trim();
@@ -84,10 +82,7 @@
       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())
@@ -103,6 +98,7 @@
       bu.addOp(rsrc.getId(), op);
 
       ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
+      reviewersAddition.op.suppressEmail();
       bu.addOp(rsrc.getId(), reviewersAddition.op);
 
       bu.execute();
@@ -111,7 +107,7 @@
   }
 
   private ReviewerAddition addAssigneeAsCC(ChangeResource rsrc, String assignee)
-      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+      throws IOException, PermissionBackendException, ConfigInvalidException {
     AddReviewerInput reviewerInput = new AddReviewerInput();
     reviewerInput.reviewer = assignee;
     reviewerInput.state = ReviewerState.CC;
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index b5c6c95..0ec38e1 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -82,7 +81,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       ChangeUpdate update = ctx.getUpdate(psId);
       newDescription = Strings.nullToEmpty(input.description);
       oldDescription = Strings.nullToEmpty(psUtil.get(ctx.getNotes(), psId).getDescription());
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 974d715..f6c9abe 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -77,7 +76,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, PermissionBackendException {
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
       return delete.applyImpl(updateFactory, rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
@@ -111,7 +110,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
+        throws ResourceNotFoundException, PatchListNotAvailableException {
       Optional<Comment> maybeComment =
           commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 80d0aba..c542164 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -29,7 +29,7 @@
 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;
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -71,7 +70,7 @@
   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
@@ -83,7 +82,7 @@
       PermissionBackend permissionBackend,
       @GerritPersonIdent PersonIdent gerritIdent,
       PatchSetUtil psUtil,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       ProjectCache projectCache) {
     super(retryHelper);
     this.repositoryManager = repositoryManager;
@@ -92,7 +91,7 @@
     this.tz = gerritIdent.getTimeZone();
     this.permissionBackend = permissionBackend;
     this.psUtil = psUtil;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
     this.projectCache = projectCache;
   }
 
@@ -100,7 +99,7 @@
   protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
-          OrmException, ConfigInvalidException {
+          ConfigInvalidException {
     PatchSet ps = psUtil.current(resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
@@ -117,11 +116,6 @@
         resource.getChange().getKey().get(),
         sanitizedCommitMessage);
 
-    NotifyHandling notify = input.notify;
-    if (notify == null) {
-      notify = resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
-    }
-
     try (Repository repository = repositoryManager.openRepository(resource.getProject());
         RevWalk revWalk = new RevWalk(repository);
         ObjectInserter objectInserter = repository.newObjectInserter()) {
@@ -145,8 +139,7 @@
         inserter.setMessage(
             String.format("Patch Set %s: Commit message was updated.", psId.getId()));
         inserter.setDescription("Edit commit message");
-        inserter.setNotify(notify);
-        inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+        bu.setNotify(resolveNotify(input, resource));
         bu.addOp(resource.getChange().getId(), inserter);
         bu.execute();
       }
@@ -154,6 +147,16 @@
     return Response.ok("ok");
   }
 
+  private NotifyResolver.Result resolveNotify(CommitMessageInput input, ChangeResource resource)
+      throws BadRequestException, ConfigInvalidException, IOException {
+    NotifyHandling notifyHandling = input.notify;
+    if (notifyHandling == null) {
+      notifyHandling =
+          resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
+    }
+    return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+  }
+
   private ObjectId createCommit(
       ObjectInserter objectInserter,
       RevCommit basePatchSetCommit,
@@ -172,8 +175,7 @@
   }
 
   private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
-          OrmException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!userProvider.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index bc272e9..abfa49b 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -93,7 +92,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException {
+    public boolean updateChange(ChangeContext ctx) {
       change = ctx.getChange();
       ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
       newTopicName = Strings.nullToEmpty(input.topic);
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index d1eea44..5ee49ff 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -31,7 +32,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -70,7 +70,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Option(
@@ -82,6 +82,11 @@
     imp.setStart(start);
   }
 
+  @Option(name = "--no-limit", usage = "Return all results, overriding the default limit")
+  public void setNoLimit(boolean on) {
+    imp.setNoLimit(on);
+  }
+
   @Override
   public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
     imp.setDynamicBean(plugin, dynamicBean);
@@ -109,7 +114,7 @@
 
   @Override
   public List<?> apply(TopLevelResource rsrc)
-      throws BadRequestException, AuthException, OrmException, PermissionBackendException {
+      throws BadRequestException, AuthException, PermissionBackendException {
     List<List<ChangeInfo>> out;
     try {
       out = query();
@@ -122,8 +127,7 @@
     return out.size() == 1 ? out.get(0) : out;
   }
 
-  private List<List<ChangeInfo>> query()
-      throws OrmException, QueryParseException, PermissionBackendException {
+  private List<List<ChangeInfo>> query() throws QueryParseException, PermissionBackendException {
     if (imp.isDisabled()) {
       throw new QueryParseException("query disabled");
     }
@@ -137,7 +141,8 @@
 
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
-    List<List<ChangeInfo>> res = json.create(options, this.imp).format(results);
+    List<List<ChangeInfo>> res =
+        json.create(options, this.imp.getAttributesFactory()).format(results);
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
       if (results.get(n).more() && !info.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index de9e990..4e6f65e 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -27,12 +28,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.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;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -99,8 +99,7 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
-      throws OrmException, UpdateException, RestApiException, IOException,
-          PermissionBackendException {
+      throws UpdateException, RestApiException, IOException, PermissionBackendException {
     // Not allowed to rebase if the current patch set is locked.
     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
@@ -114,12 +113,14 @@
         RevWalk rw = new RevWalk(reader);
         BatchUpdate bu =
             updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
-      if (!change.getStatus().isOpen()) {
+      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(),
@@ -134,7 +135,7 @@
 
   private ObjectId findBaseRev(
       Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
-      throws RestApiException, OrmException, IOException, NoSuchChangeException, AuthException,
+      throws RestApiException, IOException, NoSuchChangeException, AuthException,
           PermissionBackendException {
     Branch.NameKey destRefKey = rsrc.getChange().getDest();
     if (input == null || input.base == null) {
@@ -172,7 +173,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(
@@ -204,7 +205,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (!(change.getStatus().isOpen() && rsrc.isCurrent())) {
+    if (!(change.isNew() && rsrc.isCurrent())) {
       return description;
     }
 
@@ -222,7 +223,7 @@
       if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
@@ -234,7 +235,11 @@
       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;
@@ -261,8 +266,7 @@
     @Override
     protected ChangeInfo applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
-        throws OrmException, UpdateException, RestApiException, IOException,
-            PermissionBackendException {
+        throws UpdateException, RestApiException, IOException, PermissionBackendException {
       PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
index 6020e95..81294ed 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -50,8 +49,7 @@
 
   @Override
   protected Response<?> applyImpl(BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in)
-      throws AuthException, ResourceConflictException, IOException, OrmException,
-          PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     Project.NameKey project = rsrc.getProject();
     try (Repository repository = repositoryManager.openRepository(project)) {
       editModifier.rebaseEdit(repository, rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 8634e1d..937ab56 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -72,7 +71,7 @@
   }
 
   public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
-      throws OrmException, IOException, PermissionBackendException {
+      throws IOException, PermissionBackendException {
     checkArgument(!in.isEmpty(), "Input may not be empty");
     // Map of all patch sets, keyed by commit SHA-1.
     Map<String, PatchSetData> byId = collectById(in);
@@ -113,8 +112,7 @@
     return result;
   }
 
-  private Map<String, PatchSetData> collectById(List<ChangeData> in)
-      throws OrmException, IOException {
+  private Map<String, PatchSetData> collectById(List<ChangeData> in) throws IOException {
     Project.NameKey project = in.get(0).change().getProject();
     Map<String, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
     try (Repository repo = repoManager.openRepository(project);
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 532c10f..5f56cdb 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -83,8 +83,7 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
-          IOException {
+      throws RestApiException, UpdateException, PermissionBackendException, IOException {
     // Not allowed to restore if the current patch set is locked.
     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
 
@@ -111,9 +110,9 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx) throws OrmException, ResourceConflictException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceConflictException {
       change = ctx.getChange();
-      if (change == null || change.getStatus() != Status.ABANDONED) {
+      if (change == null || !change.isAbandoned()) {
         throw new ResourceConflictException("change is " + ChangeUtil.status(change));
       }
       PatchSet.Id psId = change.currentPatchSetId();
@@ -139,7 +138,7 @@
     }
 
     @Override
-    public void postUpdate(Context ctx) throws OrmException {
+    public void postUpdate(Context ctx) {
       try {
         ReplyToChangeSender cm = restoredSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
@@ -162,7 +161,7 @@
             .setVisible(false);
 
     Change change = rsrc.getChange();
-    if (change.getStatus() != Status.ABANDONED) {
+    if (!change.isAbandoned()) {
       return description;
     }
 
@@ -180,7 +179,7 @@
       if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
         return description;
       }
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index c81b0f8..3fe6770 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -40,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;
@@ -65,7 +64,6 @@
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -104,7 +102,7 @@
   private final ChangeReverted changeReverted;
   private final ContributorAgreementsChecker contributorAgreements;
   private final ProjectCache projectCache;
-  private final NotifyUtil notifyUtil;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   Revert(
@@ -122,7 +120,7 @@
       ChangeReverted changeReverted,
       ContributorAgreementsChecker contributorAgreements,
       ProjectCache projectCache,
-      NotifyUtil notifyUtil) {
+      NotifyResolver notifyResolver) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
@@ -137,16 +135,16 @@
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
     this.projectCache = projectCache;
-    this.notifyUtil = notifyUtil;
+    this.notifyResolver = notifyResolver;
   }
 
   @Override
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
-      throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException,
+      throws IOException, RestApiException, UpdateException, NoSuchChangeException,
           PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
     Change change = rsrc.getChange();
-    if (change.getStatus() != Change.Status.MERGED) {
+    if (!change.isMerged()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
@@ -160,7 +158,7 @@
 
   private Change.Id revert(
       BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, RevertInput input)
-      throws OrmException, IOException, RestApiException, UpdateException, ConfigInvalidException {
+      throws IOException, RestApiException, UpdateException, ConfigInvalidException {
     String message = Strings.emptyToNull(input.message);
     Change.Id changeIdToRevert = notes.getChangeId();
     PatchSet.Id patchSetId = notes.getChange().currentPatchSetId();
@@ -216,16 +214,15 @@
       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(notes);
 
@@ -240,8 +237,9 @@
 
       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();
       }
@@ -266,7 +264,7 @@
         .setTitle("Revert the change")
         .setVisible(
             and(
-                change.getStatus() == Change.Status.MERGED && projectStatePermitsWrite,
+                change.isMerged() && projectStatePermitsWrite,
                 permissionBackend
                     .user(rsrc.getUser())
                     .ref(change.getDest())
@@ -276,18 +274,10 @@
   private class NotifyOp implements BatchUpdateOp {
     private final Change change;
     private final ChangeInserter ins;
-    private final NotifyHandling notifyHandling;
-    private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
 
-    NotifyOp(
-        Change change,
-        ChangeInserter ins,
-        NotifyHandling notifyHandling,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+    NotifyOp(Change change, ChangeInserter ins) {
       this.change = change;
       this.ins = ins;
-      this.notifyHandling = notifyHandling;
-      this.accountsToNotify = accountsToNotify;
     }
 
     @Override
@@ -296,8 +286,7 @@
       try {
         RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
-        cm.setNotify(notifyHandling);
-        cm.setAccountsToNotify(accountsToNotify);
+        cm.setNotify(ctx.getNotify(change.getId()));
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewed.java b/java/com/google/gerrit/server/restapi/change/Reviewed.java
index accc355..4594503 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewed.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -36,15 +35,14 @@
     }
 
     @Override
-    public Response<String> apply(FileResource resource, Input input) throws OrmException {
+    public Response<String> apply(FileResource resource, Input input) {
       boolean reviewFlagUpdated =
           accountPatchReviewStore.call(
               s ->
                   s.markReviewed(
                       resource.getPatchKey().getParentKey(),
                       resource.getAccountId(),
-                      resource.getPatchKey().getFileName()),
-              OrmException.class);
+                      resource.getPatchKey().getFileName()));
       return reviewFlagUpdated ? Response.created("") : Response.ok("");
     }
   }
@@ -59,14 +57,13 @@
     }
 
     @Override
-    public Response<?> apply(FileResource resource, Input input) throws OrmException {
+    public Response<?> apply(FileResource resource, Input input) {
       accountPatchReviewStore.run(
           s ->
               s.clearReviewed(
                   resource.getPatchKey().getParentKey(),
                   resource.getAccountId(),
-                  resource.getPatchKey().getFileName()),
-          OrmException.class);
+                  resource.getPatchKey().getFileName()));
       return Response.none();
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index eb79b79..ed487f7 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -50,7 +49,6 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -102,7 +100,7 @@
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     logger.atFine().log("Candidates %s", candidateList);
 
     String query = suggestReviewers.getQuery();
@@ -190,10 +188,8 @@
     }
 
     // Sort results
-    Stream<Entry<Account.Id, MutableDouble>> sorted =
-        reviewerScores
-            .entrySet()
-            .stream()
+    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());
     logger.atFine().log("Sorted suggestions: %s", sortedSuggestions);
@@ -201,7 +197,7 @@
   }
 
   private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     // Get the user's last 25 changes, check approvals
     try {
       List<ChangeData> result =
@@ -231,7 +227,7 @@
 
   private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
       List<Account.Id> candidates, ProjectState projectState, double baseWeight)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     // Get each reviewer's activity based on number of applied labels
     // (weighted 10d), number of comments (weighted 0.5d) and number of owned
     // changes (weighted 1d).
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index cf69080..546ca01 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -27,7 +27,6 @@
 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.Singleton;
 import java.io.IOException;
@@ -68,8 +67,7 @@
 
   @Override
   public ReviewerResource parse(ChangeResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException, IOException,
-          ConfigInvalidException {
+      throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
     Address address = Address.tryParse(id.get());
 
     Account.Id accountId = null;
@@ -93,7 +91,7 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) throws OrmException {
+  private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) {
     return approvalsUtil.getReviewers(rsrc.getNotes()).all();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 75710c4..779f277 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -57,7 +57,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -156,7 +155,7 @@
   }
 
   public interface VisibilityControl {
-    boolean isVisibleTo(Account.Id account) throws OrmException;
+    boolean isVisibleTo(Account.Id account);
   }
 
   public List<SuggestedReviewerInfo> suggestReviewers(
@@ -165,7 +164,7 @@
       ProjectState projectState,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
-      throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     CurrentUser currentUser = self.get();
     if (changeNotes != null) {
       logger.atFine().log(
@@ -224,7 +223,7 @@
     return suggestedReviewers;
   }
 
-  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) throws OrmException {
+  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) {
     try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
       try {
         // For performance reasons we don't use AccountQueryProvider as it would always load the
@@ -247,9 +246,7 @@
                         ImmutableSet.of(AccountField.ID.getName())))
                 .readRaw();
         List<Account.Id> matches =
-            result
-                .toList()
-                .stream()
+            result.toList().stream()
                 .map(f -> new Account.Id(f.getValue(AccountField.ID).intValue()))
                 .collect(toList());
         logger.atFine().log("Matches: %s", matches);
@@ -266,7 +263,7 @@
       VisibilityControl visibilityControl,
       boolean excludeGroups,
       List<Account.Id> filteredRecommendations)
-      throws OrmException, PermissionBackendException, IOException {
+      throws PermissionBackendException, IOException {
     List<SuggestedReviewerInfo> suggestedReviewers = loadAccounts(filteredRecommendations);
 
     int limit = suggestReviewers.getLimit();
@@ -295,7 +292,7 @@
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
       return reviewerRecommender.suggestReviewers(
           changeNotes, suggestReviewers, projectState, candidateList);
@@ -310,8 +307,7 @@
 
     try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
       List<SuggestedReviewerInfo> reviewer =
-          accountIds
-              .stream()
+          accountIds.stream()
               .map(accountLoader::get)
               .filter(Objects::nonNull)
               .map(
@@ -332,7 +328,7 @@
       ProjectState projectState,
       VisibilityControl visibilityControl,
       int limit)
-      throws OrmException, IOException {
+      throws IOException {
     try (Timer0.Context ctx = metrics.queryGroupsLatency.start()) {
       List<SuggestedReviewerInfo> groups = new ArrayList<>();
       for (GroupReference g : suggestAccountGroups(suggestReviewers, projectState)) {
@@ -378,7 +374,7 @@
       Project project,
       GroupReference group,
       VisibilityControl visibilityControl)
-      throws OrmException, IOException {
+      throws IOException {
     GroupAsReviewer result = new GroupAsReviewer();
     int maxAllowed = suggestReviewers.getMaxAllowed();
     int maxAllowedWithoutConfirmation = suggestReviewers.getMaxAllowedWithoutConfirmation();
@@ -432,8 +428,7 @@
   }
 
   private static String formatSuggestedReviewers(List<SuggestedReviewerInfo> suggestedReviewers) {
-    return suggestedReviewers
-        .stream()
+    return suggestedReviewers.stream()
         .map(
             r -> {
               if (r.account != null) {
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index 60c9a54..a41143c 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -28,7 +28,6 @@
 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.Singleton;
 import java.io.IOException;
@@ -69,8 +68,8 @@
 
   @Override
   public ReviewerResource parse(RevisionResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException,
-          IOException, ConfigInvalidException {
+      throws ResourceNotFoundException, AuthException, MethodNotAllowedException, IOException,
+          ConfigInvalidException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot access on non-current patch set");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 4edd741..c5cce4f 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -77,12 +76,11 @@
 
   @Override
   public RevisionResource parse(ChangeResource change, IdString id)
-      throws ResourceNotFoundException, AuthException, OrmException, IOException,
-          PermissionBackendException {
+      throws ResourceNotFoundException, AuthException, IOException, PermissionBackendException {
     if (id.get().equals("current")) {
       PatchSet ps = psUtil.current(change.getNotes());
       if (ps != null && visible(change)) {
-        return RevisionResource.createNonCachable(change, ps);
+        return RevisionResource.createNonCacheable(change, ps);
       }
       throw new ResourceNotFoundException(id);
     }
@@ -117,7 +115,7 @@
   }
 
   private List<RevisionResource> find(ChangeResource change, String id)
-      throws OrmException, IOException, AuthException {
+      throws IOException, AuthException {
     if (id.equals("0") || id.equals("edit")) {
       return loadEdit(change, null);
     } else if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
@@ -142,8 +140,7 @@
     }
   }
 
-  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id)
-      throws OrmException {
+  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
     PatchSet ps =
         psUtil.get(change.getNotes(), new PatchSet.Id(change.getId(), Integer.parseInt(id)));
     if (ps != null) {
diff --git a/java/com/google/gerrit/server/restapi/change/RobotComments.java b/java/com/google/gerrit/server/restapi/change/RobotComments.java
index 6570ae0..1aa8c2a 100644
--- a/java/com/google/gerrit/server/restapi/change/RobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/RobotComments.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -56,7 +55,7 @@
 
   @Override
   public RobotCommentResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException {
     String uuid = id.get();
     ChangeNotes notes = rev.getNotes();
 
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 6d25d938..aacf58b 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -14,60 +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.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 PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetReadyForReview(
-      RetryHelper retryHelper,
-      WorkInProgressOp.Factory opFactory,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetReadyForReview(RetryHelper retryHelper, WorkInProgressOp.Factory opFactory) {
     super(retryHelper);
     this.opFactory = opFactory;
-    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));
     }
 
@@ -77,6 +66,7 @@
 
     try (BatchUpdate bu =
         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("");
@@ -90,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 a23f591..852813e 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -14,60 +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.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 PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetWorkInProgress(
-      WorkInProgressOp.Factory opFactory,
-      RetryHelper retryHelper,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetWorkInProgress(WorkInProgressOp.Factory opFactory, RetryHelper retryHelper) {
     super(retryHelper);
     this.opFactory = opFactory;
-    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));
     }
 
@@ -77,6 +66,7 @@
 
     try (BatchUpdate bu =
         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("");
@@ -90,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 a0e9d70..d12d7c6 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -59,8 +60,6 @@
 import com.google.gerrit.server.submit.MergeOp;
 import com.google.gerrit.server.submit.MergeSuperSet;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +68,6 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -176,8 +174,8 @@
 
   @Override
   public Output apply(RevisionResource rsrc, SubmitInput input)
-      throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-          PermissionBackendException, UpdateException, ConfigInvalidException {
+      throws RestApiException, RepositoryNotFoundException, IOException, PermissionBackendException,
+          UpdateException, ConfigInvalidException {
     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
     IdentifiedUser submitter;
     if (input.onBehalfOf != null) {
@@ -192,10 +190,10 @@
   }
 
   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException,
+      throws RestApiException, IOException, UpdateException, ConfigInvalidException,
           PermissionBackendException {
     Change change = rsrc.getChange();
-    if (!change.getStatus().isOpen()) {
+    if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
       throw new ResourceConflictException(
@@ -209,23 +207,23 @@
 
     try (MergeOp op = mergeOpProvider.get()) {
       op.merge(change, submitter, true, input, false);
-      try {
-        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));
+    // Read the ChangeNotes only after MergeOp is fully done (including MergeOp#close) to be sure
+    // to have the correct state of the repo.
+    try {
+      change = changeNotesFactory.createChecked(change.getProject(), change.getId()).getChange();
+    } catch (NoSuchChangeException e) {
+      throw new ResourceConflictException("change is deleted");
     }
+
+    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));
   }
 
   /**
@@ -290,9 +288,9 @@
         return "Problems with change(s): "
             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
       }
-    } catch (PermissionBackendException | OrmException | IOException e) {
+    } catch (PermissionBackendException | IOException e) {
       logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
+      throw new StorageException("Could not determine problems for the change", e);
     }
     return null;
   }
@@ -300,7 +298,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)) {
@@ -313,7 +311,7 @@
       }
     } catch (IOException e) {
       logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
+      throw new StorageException("Could not determine problems for the change", e);
     }
 
     ChangeData cd = changeDataFactory.create(resource.getNotes());
@@ -321,42 +319,31 @@
       MergeOp.checkSubmitRule(cd, false);
     } catch (ResourceConflictException e) {
       return null; // submit not visible
-    } catch (OrmException e) {
-      logger.atSevere().withCause(e).log("Error checking if change is submittable");
-      throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
     ChangeSet cs;
     try {
       cs = mergeSuperSet.get().completeChangeSet(cd.change(), resource.getUser());
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new OrmRuntimeException(
-          "Could not determine complete set of changes to be submitted", e);
+    } catch (IOException | PermissionBackendException e) {
+      throw new StorageException("Could not determine complete set of changes to be submitted", e);
     }
 
     String topic = change.getTopic();
     int topicSize = 0;
     if (!Strings.isNullOrEmpty(topic)) {
-      topicSize = getChangesByTopic(topic).size();
+      topicSize = queryProvider.get().noFields().byTopicOpen(topic).size();
     }
     boolean treatWithTopic = submitWholeTopic && !Strings.isNullOrEmpty(topic) && topicSize > 1;
 
     String submitProblems = problemsForSubmittingChangeset(cd, cs, resource.getUser());
 
-    Boolean enabled;
-    try {
-      // Recheck mergeability rather than using value stored in the index,
-      // which may be stale.
-      // TODO(dborowitz): This is ugly; consider providing a way to not read
-      // stored fields from the index in the first place.
-      // cd.setMergeable(null);
-      // That was done in unmergeableChanges which was called by
-      // problemsForSubmittingChangeset, so now it is safe to read from
-      // the cache, as it yields the same result.
-      enabled = cd.isMergeable();
-    } catch (OrmException e) {
-      throw new OrmRuntimeException("Could not determine mergeability", e);
-    }
+    // Recheck mergeability rather than using value stored in the index, which may be stale.
+    // TODO(dborowitz): This is ugly; consider providing a way to not read stored fields from the
+    // index in the first place.
+    // cd.setMergeable(null);
+    // That was done in unmergeableChanges which was called by problemsForSubmittingChangeset, so
+    // now it is safe to read from the cache, as it yields the same result.
+    Boolean enabled = cd.isMergeable();
 
     if (submitProblems != null) {
       return new UiAction.Description()
@@ -392,7 +379,7 @@
         .setEnabled(Boolean.TRUE.equals(enabled));
   }
 
-  public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws OrmException, IOException {
+  public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws IOException {
     Set<ChangeData> mergeabilityMap = new HashSet<>();
     for (ChangeData change : cs.changes()) {
       mergeabilityMap.add(change);
@@ -441,7 +428,7 @@
   }
 
   private HashMap<Change.Id, RevCommit> findCommits(
-      Collection<ChangeData> changes, Project.NameKey project) throws IOException, OrmException {
+      Collection<ChangeData> changes, Project.NameKey project) throws IOException {
     HashMap<Change.Id, RevCommit> commits = new HashMap<>();
     try (Repository repo = repoManager.openRepository(project);
         RevWalk walk = new RevWalk(repo)) {
@@ -456,14 +443,15 @@
   }
 
   private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
-      throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException,
-          IOException, ConfigInvalidException {
+      throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException,
+          ConfigInvalidException {
     PermissionBackend.ForChange perm = rsrc.permissions();
     perm.check(ChangePermission.SUBMIT);
     perm.check(ChangePermission.SUBMIT_AS);
 
     CurrentUser caller = rsrc.getUser();
-    IdentifiedUser submitter = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
+    IdentifiedUser submitter =
+        accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
     try {
       permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ);
     } catch (AuthException e) {
@@ -473,14 +461,6 @@
     return submitter;
   }
 
-  private List<ChangeData> getChangesByTopic(String topic) {
-    try {
-      return queryProvider.get().byTopicOpen(topic);
-    } catch (OrmException e) {
-      throw new OrmRuntimeException(e);
-    }
-  }
-
   public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> {
     private final Submit submit;
     private final ChangeJson.Factory json;
@@ -495,7 +475,7 @@
 
     @Override
     public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
-        throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
+        throws RestApiException, RepositoryNotFoundException, IOException,
             PermissionBackendException, UpdateException, ConfigInvalidException {
       PatchSet ps = psUtil.current(rsrc.getNotes());
       if (ps == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index f6bd8ad..efefc3e 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -19,9 +19,9 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
-import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -109,7 +108,7 @@
   @Override
   public Object apply(ChangeResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
-          OrmException, PermissionBackendException {
+          PermissionBackendException {
     SubmittedTogetherInfo info = applyInfo(resource);
     if (options.isEmpty()) {
       return info.changes;
@@ -118,17 +117,17 @@
   }
 
   public SubmittedTogetherInfo applyInfo(ChangeResource resource)
-      throws AuthException, IOException, OrmException, PermissionBackendException {
+      throws AuthException, IOException, PermissionBackendException {
     Change c = resource.getChange();
     try {
       List<ChangeData> cds;
       int hidden;
 
-      if (c.getStatus().isOpen()) {
+      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 {
@@ -145,13 +144,13 @@
       info.changes = json.create(jsonOpt).format(cds);
       info.nonVisibleChanges = hidden;
       return info;
-    } catch (OrmException | IOException e) {
+    } catch (StorageException | IOException e) {
       logger.atSevere().withCause(e).log("Error on getting a ChangeSet");
       throw e;
     }
   }
 
-  private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws OrmException, IOException {
+  private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
     if (cds.size() <= 1 && hidden == 0) {
       // Skip sorting for singleton lists, to avoid WalkSorter opening the
       // repo just to fill out the commit field in PatchSetData.
@@ -176,8 +175,7 @@
     return sorted;
   }
 
-  private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds)
-      throws OrmException {
+  private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) {
     // TODO(hiesel): Instead of calling these manually, either implement a helper that brings a
     // database-backed change on-par with an index-backed change in terms of the populated fields in
     // ChangeData or check if any of the ChangeDatas was loaded from the database and allow
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 708d914..f5a2751 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -65,7 +64,7 @@
 
   @Override
   public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index 7cba8e7..4904da7 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.PrologRule;
 import com.google.gerrit.server.rules.RulesCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -70,7 +69,7 @@
 
   @Override
   public List<TestSubmitRuleInfo> apply(RevisionResource rsrc, TestSubmitRuleInput input)
-      throws AuthException, OrmException, PermissionBackendException, BadRequestException {
+      throws AuthException, PermissionBackendException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index 684d22f..46dbad6 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.RulesCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import org.kohsuke.args4j.Option;
 
@@ -52,7 +51,7 @@
 
   @Override
   public SubmitType apply(RevisionResource rsrc, TestSubmitRuleInput input)
-      throws AuthException, BadRequestException, OrmException {
+      throws AuthException, BadRequestException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
@@ -88,8 +87,7 @@
     }
 
     @Override
-    public SubmitType apply(RevisionResource resource)
-        throws AuthException, BadRequestException, OrmException {
+    public SubmitType apply(RevisionResource resource) throws AuthException, BadRequestException {
       return test.apply(resource, null);
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Unignore.java b/java/com/google/gerrit/server/restapi/change/Unignore.java
index 6f2144a..26d3233 100644
--- a/java/com/google/gerrit/server/restapi/change/Unignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Unignore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -22,7 +23,6 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -46,8 +46,7 @@
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws OrmException, IllegalLabelException {
+  public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException {
     if (isIgnored(rsrc)) {
       stars.unignore(rsrc);
     }
@@ -57,7 +56,7 @@
   private boolean isIgnored(ChangeResource rsrc) {
     try {
       return stars.isIgnored(rsrc);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
diff --git a/java/com/google/gerrit/server/restapi/change/Votes.java b/java/com/google/gerrit/server/restapi/change/Votes.java
index a5d77e9..31efe54 100644
--- a/java/com/google/gerrit/server/restapi/change/Votes.java
+++ b/java/com/google/gerrit/server/restapi/change/Votes.java
@@ -26,7 +26,6 @@
 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.Singleton;
 import java.util.Map;
@@ -55,7 +54,7 @@
 
   @Override
   public VoteResource parse(ReviewerResource reviewer, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException, MethodNotAllowedException {
+      throws ResourceNotFoundException, AuthException, MethodNotAllowedException {
     if (reviewer.getRevisionResource() != null && !reviewer.getRevisionResource().isCurrent()) {
       throw new MethodNotAllowedException("Cannot access on non-current patch set");
     }
@@ -72,8 +71,7 @@
     }
 
     @Override
-    public Map<String, Short> apply(ReviewerResource rsrc)
-        throws OrmException, MethodNotAllowedException {
+    public Map<String, Short> apply(ReviewerResource rsrc) throws MethodNotAllowedException {
       if (rsrc.getRevisionResource() != null && !rsrc.getRevisionResource().isCurrent()) {
         throw new MethodNotAllowedException("Cannot list votes on non-current patch set");
       }
diff --git a/java/com/google/gerrit/server/restapi/config/AgreementJson.java b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
index 02e5f68..d5c085b 100644
--- a/java/com/google/gerrit/server/restapi/config/AgreementJson.java
+++ b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
@@ -17,7 +17,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AgreementInfo;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -61,7 +61,7 @@
         GroupControl gc = genericGroupControlFactory.controlFor(user, autoVerifyGroup.getUUID());
         GroupResource group = new GroupResource(gc);
         info.autoVerifyGroup = groupJson.format(group);
-      } catch (NoSuchGroupException | OrmException e) {
+      } catch (NoSuchGroupException | StorageException e) {
         logger.atWarning().log(
             "autoverify group \"%s\" does not exist, referenced in CLA \"%s\"",
             autoVerifyGroup.getName(), ca.getName());
diff --git a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index a16736b..61d5c79 100644
--- a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -56,8 +55,7 @@
 
   @Override
   public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
-      throws RestApiException, IOException, OrmException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
 
     if (input == null
diff --git a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
index 5a1592f..152a4db 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.restapi.config.ConfirmEmail.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,8 +54,7 @@
 
   @Override
   public Response<?> apply(ConfigResource rsrc, Input input)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+      throws AuthException, UnprocessableEntityException, IOException, ConfigInvalidException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index c03c4c5..43bfa81 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -298,6 +298,7 @@
     info.docSearch = docSearcher.isAvailable();
     info.editGpgKeys =
         toBoolean(enableSignedPush && config.getBoolean("gerrit", null, "editGpgKeys", true));
+    info.primaryWeblinkName = config.getString("gerrit", null, "primaryWeblinkName");
     return info;
   }
 
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/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index f77cda4..7402c15 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -106,9 +106,7 @@
   }
 
   private List<TaskInfo> getTasks() {
-    return workQueue
-        .getTaskInfos(TaskInfo::new)
-        .stream()
+    return workQueue.getTaskInfos(TaskInfo::new).stream()
         .sorted(
             comparing((TaskInfo t) -> t.state.ordinal())
                 .thenComparing(t -> t.delay)
diff --git a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
index c296a7d..2feb580 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.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;
@@ -33,9 +35,9 @@
   }
 
   @Override
-  public List<TopMenu.MenuEntry> apply(ConfigResource resource) {
+  public Response<List<MenuEntry>> apply(ConfigResource resource) {
     List<TopMenu.MenuEntry> entries = new ArrayList<>();
     extensions.runEach(extension -> entries.addAll(extension.getEntries()));
-    return entries;
+    return Response.ok(entries).caching(ConfigResource.DEFAULT_CACHE_CONTROL);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
index cab07e3..0685a58 100644
--- a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -54,10 +54,7 @@
     if (updates.isEmpty()) {
       return Collections.emptyMap();
     }
-    return updates
-        .asMap()
-        .entrySet()
-        .stream()
+    return updates.asMap().entrySet().stream()
         .collect(
             Collectors.toMap(
                 e -> e.getKey().name().toLowerCase(), e -> toEntryInfos(e.getValue())));
@@ -65,8 +62,7 @@
 
   private static List<ConfigUpdateEntryInfo> toEntryInfos(
       Collection<ConfigUpdateEntry> updateEntries) {
-    return updateEntries
-        .stream()
+    return updateEntries.stream()
         .map(ReloadConfig::toConfigUpdateEntryInfo)
         .collect(toImmutableList());
   }
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index bdf1c74..a841897 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.GroupControl;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddMembers.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -112,9 +112,8 @@
 
   @Override
   public List<AccountInfo> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
-          IOException, ConfigInvalidException, ResourceNotFoundException,
-          PermissionBackendException {
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException,
+          ConfigInvalidException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -144,19 +143,18 @@
   }
 
   Account findAccount(String nameOrEmailOrId)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+      throws UnprocessableEntityException, IOException, ConfigInvalidException {
+    AccountResolver.Result result = accountResolver.resolve(nameOrEmailOrId);
     try {
-      return accountResolver.parse(nameOrEmailOrId).getAccount();
-    } catch (UnprocessableEntityException e) {
-      // might be because the account does not exist or because the account is
-      // not visible
+      return result.asUnique().getAccount();
+    } catch (UnresolvableAccountException e) {
       switch (authType) {
         case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
         case LDAP:
-          if (accountResolver.find(nameOrEmailOrId) == null) {
-            // account does not exist, try to create it
+          if (!e.isSelf() && result.asList().isEmpty()) {
+            // Account does not exist, try to create it. This may leak account existence, since we
+            // can't distinguish between a nonexistent account and one that the caller can't see.
             Optional<Account> a = createAccountByLdap(nameOrEmailOrId);
             if (a.isPresent()) {
               return a.get();
@@ -177,7 +175,7 @@
   }
 
   public void addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> newMemberIds)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, newMemberIds))
@@ -224,8 +222,8 @@
 
     @Override
     public AccountInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException, PermissionBackendException {
+        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+            ConfigInvalidException, PermissionBackendException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id.get();
       try {
@@ -251,7 +249,7 @@
 
     @Override
     public AccountInfo apply(MemberResource resource, Input input)
-        throws OrmException, PermissionBackendException {
+        throws PermissionBackendException {
       // Do nothing, the user is already a member.
       return get.apply(resource);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index 9782ad3..9f9deff 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddSubgroups.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -93,7 +92,7 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource resource, Input input)
-      throws NotInternalGroupException, AuthException, UnprocessableEntityException, OrmException,
+      throws NotInternalGroupException, AuthException, UnprocessableEntityException,
           ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     GroupDescription.Internal group =
@@ -124,7 +123,7 @@
 
   private void addSubgroups(
       AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> newSubgroupUuids)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setSubgroupModification(subgroupUuids -> Sets.union(subgroupUuids, newSubgroupUuids))
@@ -144,8 +143,8 @@
 
     @Override
     public GroupInfo apply(GroupResource resource, IdString id, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException, PermissionBackendException {
+        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, IOException,
+            ConfigInvalidException, PermissionBackendException {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(id.get());
       try {
@@ -171,7 +170,7 @@
 
     @Override
     public GroupInfo apply(SubgroupResource resource, Input input)
-        throws OrmException, PermissionBackendException {
+        throws PermissionBackendException {
       // Do nothing, the group is already included.
       return get.get().apply(resource);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 2fb668f..ffa462d 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.client.ListGroupsOption;
@@ -36,7 +37,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.GroupCache;
@@ -50,12 +50,11 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -125,8 +124,8 @@
   @Override
   public GroupInfo apply(TopLevelResource resource, IdString id, GroupInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
-          ResourceConflictException, OrmException, IOException, ConfigInvalidException,
-          ResourceNotFoundException, PermissionBackendException {
+          ResourceConflictException, IOException, ConfigInvalidException, ResourceNotFoundException,
+          PermissionBackendException {
     String name = id.get();
     if (input == null) {
       input = new GroupInput();
@@ -178,7 +177,7 @@
   }
 
   private InternalGroup createGroup(CreateGroupArgs createGroupArgs)
-      throws OrmException, ResourceConflictException, IOException, ConfigInvalidException {
+      throws ResourceConflictException, IOException, ConfigInvalidException {
 
     String nameLower = createGroupArgs.getGroupName().toLowerCase(Locale.US);
 
@@ -218,7 +217,7 @@
         members -> ImmutableSet.copyOf(createGroupArgs.initialMembers));
     try {
       return groupsUpdateProvider.get().createGroup(groupCreation, groupUpdateBuilder.build());
-    } catch (OrmDuplicateKeyException e) {
+    } catch (DuplicateKeyException e) {
       throw new ResourceConflictException(
           "group '" + createGroupArgs.getGroupName() + "' already exists");
     }
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index d197cb8..b448631 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.restapi.group.AddMembers.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,8 +55,8 @@
 
   @Override
   public Response<?> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
-          IOException, ConfigInvalidException, ResourceNotFoundException {
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException,
+          ConfigInvalidException, ResourceNotFoundException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -69,8 +68,7 @@
 
     Set<Account.Id> membersToRemove = new HashSet<>();
     for (String nameOrEmail : input.members) {
-      Account a = accountResolver.parse(nameOrEmail).getAccount();
-      membersToRemove.add(a.getId());
+      membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
     }
     AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
     try {
@@ -83,7 +81,7 @@
   }
 
   private void removeGroupMembers(AccountGroup.UUID groupUuid, Set<Account.Id> accountIds)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+      throws IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.difference(memberIds, accountIds))
@@ -103,8 +101,8 @@
 
     @Override
     public Response<?> apply(MemberResource resource, Input input)
-        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-            IOException, ConfigInvalidException, ResourceNotFoundException {
+        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, IOException,
+            ConfigInvalidException, ResourceNotFoundException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = resource.getMember().getAccountId().toString();
       return delete.get().apply(resource, in);
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index c486af4..5821be5 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.restapi.group.AddSubgroups.Input;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,7 +55,7 @@
 
   @Override
   public Response<?> apply(GroupResource resource, Input input)
-      throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
+      throws AuthException, NotInternalGroupException, UnprocessableEntityException,
           ResourceNotFoundException, IOException, ConfigInvalidException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
@@ -86,7 +85,7 @@
 
   private void removeSubgroups(
       AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> removedSubgroupUuids)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setSubgroupModification(
@@ -107,7 +106,7 @@
 
     @Override
     public Response<?> apply(SubgroupResource resource, Input input)
-        throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
+        throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
             ResourceNotFoundException, IOException, ConfigInvalidException {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(resource.getMember().get());
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index dcdd8a8..1a781d9 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -76,8 +75,8 @@
 
   @Override
   public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
-      throws AuthException, NotInternalGroupException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, NotInternalGroupException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!rsrc.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/GetDetail.java b/java/com/google/gerrit/server/restapi/group/GetDetail.java
index 75d1e34..c757383 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDetail.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(GroupResource rsrc) throws PermissionBackendException {
     return json.format(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetGroup.java b/java/com/google/gerrit/server/restapi/group/GetGroup.java
index c6cddb6..3ae447b 100644
--- a/java/com/google/gerrit/server/restapi/group/GetGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetGroup.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(GroupResource resource) throws PermissionBackendException {
     return json.format(resource.getGroup());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetMember.java b/java/com/google/gerrit/server/restapi/group/GetMember.java
index 95063de..63a8a1b 100644
--- a/java/com/google/gerrit/server/restapi/group/GetMember.java
+++ b/java/com/google/gerrit/server/restapi/group/GetMember.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.group.MemberResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,7 +32,7 @@
   }
 
   @Override
-  public AccountInfo apply(MemberResource rsrc) throws OrmException, PermissionBackendException {
+  public AccountInfo apply(MemberResource rsrc) throws PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getMember().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/group/GetOwner.java b/java/com/google/gerrit/server/restapi/group/GetOwner.java
index 0906ce6..0f0417e 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOwner.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -40,8 +39,7 @@
 
   @Override
   public GroupInfo apply(GroupResource resource)
-      throws NotInternalGroupException, ResourceNotFoundException, OrmException,
-          PermissionBackendException {
+      throws NotInternalGroupException, ResourceNotFoundException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     try {
diff --git a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
index 16e2739..4466180 100644
--- a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.SubgroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -32,7 +31,7 @@
   }
 
   @Override
-  public GroupInfo apply(SubgroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo apply(SubgroupResource rsrc) throws PermissionBackendException {
     return json.format(rsrc.getMemberDescription());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index a51fad2..12b9d61 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Collection;
@@ -76,18 +75,17 @@
     return this;
   }
 
-  public GroupInfo format(GroupResource rsrc) throws OrmException, PermissionBackendException {
+  public GroupInfo format(GroupResource rsrc) throws PermissionBackendException {
     return createGroupInfo(rsrc.getGroup(), rsrc::getControl);
   }
 
-  public GroupInfo format(GroupDescription.Basic group)
-      throws OrmException, PermissionBackendException {
+  public GroupInfo format(GroupDescription.Basic group) throws PermissionBackendException {
     return createGroupInfo(group, Suppliers.memoize(() -> groupControlFactory.controlFor(group)));
   }
 
   private GroupInfo createGroupInfo(
       GroupDescription.Basic group, Supplier<GroupControl> groupControlSupplier)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     GroupInfo info = createBasicGroupInfo(group);
 
     if (group instanceof GroupDescription.Internal) {
@@ -110,7 +108,7 @@
       GroupInfo info,
       GroupDescription.Internal internalGroup,
       Supplier<GroupControl> groupControlSupplier)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     info.description = Strings.emptyToNull(internalGroup.getDescription());
     info.groupId = internalGroup.getId().get();
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 968a7dd..9f2a7b7 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -23,8 +23,9 @@
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.account.GetGroups;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -202,7 +202,7 @@
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListOption.fromBits(ListGroupsOption.class, Integer.parseInt(hex, 16)));
   }
 
   @Option(name = "--owned-by", usage = "list groups owned by the given group uuid")
@@ -248,8 +248,7 @@
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
@@ -259,8 +258,7 @@
   }
 
   public List<GroupInfo> get()
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!Strings.isNullOrEmpty(suggest)) {
       return suggestGroups();
     }
@@ -285,7 +283,7 @@
   }
 
   private List<GroupInfo> getAllGroups()
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<GroupDescription.Internal> existingGroups =
         getAllExistingGroups()
@@ -308,8 +306,7 @@
 
   private Stream<GroupReference> getAllExistingGroups() throws IOException, ConfigInvalidException {
     if (!projects.isEmpty()) {
-      return projects
-          .stream()
+      return projects.stream()
           .map(ProjectState::getAllGroups)
           .flatMap(Collection::stream)
           .distinct();
@@ -317,8 +314,7 @@
     return groups.getAllGroupReferences();
   }
 
-  private List<GroupInfo> suggestGroups()
-      throws OrmException, BadRequestException, PermissionBackendException {
+  private List<GroupInfo> suggestGroups() throws BadRequestException, PermissionBackendException {
     if (conflictingSuggestParameters()) {
       throw new BadRequestException(
           "You should only have no more than one --project and -n with --suggest");
@@ -374,7 +370,7 @@
   }
 
   private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<? extends GroupDescription.Internal> foundGroups =
         groups
@@ -402,14 +398,13 @@
   }
 
   private List<GroupInfo> getGroupsOwnedBy(String id)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     String uuid = groupResolver.parse(id).getGroupUUID().get();
     return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
   }
 
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
-      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+      throws IOException, ConfigInvalidException, PermissionBackendException {
     return filterGroupsOwnedBy(group -> isOwner(user, group));
   }
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 4742644..8f58de2 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -67,7 +66,7 @@
 
   @Override
   public List<AccountInfo> apply(GroupResource resource)
-      throws NotInternalGroupException, OrmException, PermissionBackendException {
+      throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (recursive) {
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index 864b01b..bb72a10 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -19,14 +19,13 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -47,7 +46,7 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource rsrc)
-      throws NotInternalGroupException, OrmException, PermissionBackendException {
+      throws NotInternalGroupException, PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
@@ -56,7 +55,7 @@
 
   public List<GroupInfo> getDirectSubgroups(
       GroupDescription.Internal group, GroupControl groupControl)
-      throws OrmException, PermissionBackendException {
+      throws PermissionBackendException {
     boolean ownerOfParent = groupControl.isOwner();
     List<GroupInfo> included = new ArrayList<>();
     for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
diff --git a/java/com/google/gerrit/server/restapi/group/MembersCollection.java b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
index fec1443..6dfb2b6 100644
--- a/java/com/google/gerrit/server/restapi/group/MembersCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.MemberResource;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,8 +55,8 @@
 
   @Override
   public MemberResource parse(GroupResource parent, IdString id)
-      throws NotInternalGroupException, AuthException, ResourceNotFoundException, OrmException,
-          IOException, ConfigInvalidException {
+      throws NotInternalGroupException, AuthException, ResourceNotFoundException, IOException,
+          ConfigInvalidException {
     GroupDescription.Internal group =
         parent.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
diff --git a/java/com/google/gerrit/server/restapi/group/Module.java b/java/com/google/gerrit/server/restapi/group/Module.java
index 741c3da..45ac411 100644
--- a/java/com/google/gerrit/server/restapi/group/Module.java
+++ b/java/com/google/gerrit/server/restapi/group/Module.java
@@ -82,7 +82,7 @@
   @Provides
   @ServerInitiated
   GroupsUpdate provideServerInitiatedGroupsUpdate(GroupsUpdate.Factory groupsUpdateFactory) {
-    return groupsUpdateFactory.create(null);
+    return groupsUpdateFactory.createWithServerIdent();
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/restapi/group/PutDescription.java b/java/com/google/gerrit/server/restapi/group/PutDescription.java
index dbc124b..c9078b0 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.DescriptionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,8 +45,8 @@
 
   @Override
   public Response<String> apply(GroupResource resource, DescriptionInput input)
-      throws AuthException, NotInternalGroupException, ResourceNotFoundException, OrmException,
-          IOException, ConfigInvalidException {
+      throws AuthException, NotInternalGroupException, ResourceNotFoundException, IOException,
+          ConfigInvalidException {
     if (input == null) {
       input = new DescriptionInput(); // Delete would set description to null.
     }
diff --git a/java/com/google/gerrit/server/restapi/group/PutName.java b/java/com/google/gerrit/server/restapi/group/PutName.java
index 1f1968a..319dc45 100644
--- a/java/com/google/gerrit/server/restapi/group/PutName.java
+++ b/java/com/google/gerrit/server/restapi/group/PutName.java
@@ -16,7 +16,8 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.NameInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,7 +79,7 @@
       groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
-    } catch (OrmDuplicateKeyException e) {
+    } catch (DuplicateKeyException e) {
       throw new ResourceConflictException("group with name " + newName + " already exists");
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/group/PutOptions.java b/java/com/google/gerrit/server/restapi/group/PutOptions.java
index 29b87d2..267f414 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOptions.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -45,7 +44,7 @@
   @Override
   public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input)
       throws NotInternalGroupException, AuthException, BadRequestException,
-          ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
+          ResourceNotFoundException, IOException, ConfigInvalidException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!resource.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 6ebec05..4cdad38 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.groups.OwnerInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,8 +56,8 @@
   @Override
   public GroupInfo apply(GroupResource resource, OwnerInput input)
       throws ResourceNotFoundException, NotInternalGroupException, AuthException,
-          BadRequestException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+          BadRequestException, UnprocessableEntityException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!resource.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index fa9285d..3ab0720 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.group.GroupQueryBuilder;
 import com.google.gerrit.server.query.group.GroupQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.EnumSet;
@@ -82,7 +82,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
@@ -95,8 +95,7 @@
 
   @Override
   public List<GroupInfo> apply(TopLevelResource resource)
-      throws BadRequestException, MethodNotAllowedException, OrmException,
-          PermissionBackendException {
+      throws BadRequestException, MethodNotAllowedException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 1664635..67b68b5 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.AccountResolver;
@@ -35,7 +34,6 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -62,8 +60,7 @@
 
   @Override
   public AccessCheckInfo apply(ProjectResource rsrc, AccessCheckInput input)
-      throws OrmException, PermissionBackendException, RestApiException, IOException,
-          ConfigInvalidException {
+      throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
     permissionBackend.user(rsrc.getUser()).check(GlobalPermission.VIEW_ACCESS);
 
     rsrc.getProjectState().checkStatePermitsRead();
@@ -75,20 +72,16 @@
       throw new BadRequestException("input requires 'account'");
     }
 
-    Account match = accountResolver.find(input.account);
-    if (match == null) {
-      throw new UnprocessableEntityException(
-          String.format("cannot find account %s", input.account));
-    }
+    Account.Id match = accountResolver.resolve(input.account).asUnique().getAccount().getId();
 
     AccessCheckInfo info = new AccessCheckInfo();
     try {
       permissionBackend
-          .absentUser(match.getId())
+          .absentUser(match)
           .project(rsrc.getNameKey())
           .check(ProjectPermission.ACCESS);
     } catch (AuthException e) {
-      info.message = String.format("user %s cannot see project %s", match.getId(), rsrc.getName());
+      info.message = String.format("user %s cannot see project %s", match, rsrc.getName());
       info.status = HttpServletResponse.SC_FORBIDDEN;
       return info;
     }
@@ -112,7 +105,7 @@
     if (!Strings.isNullOrEmpty(input.ref)) {
       try {
         permissionBackend
-            .absentUser(match.getId())
+            .absentUser(match)
             .ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
             .check(refPerm);
       } catch (AuthException e) {
@@ -120,7 +113,7 @@
         info.message =
             String.format(
                 "user %s lacks permission %s for %s in project %s",
-                match.getId(), input.permission, input.ref, rsrc.getName());
+                match, input.permission, input.ref, rsrc.getName());
         return info;
       }
     } else {
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
index 0aab0e1..770e8c3 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -50,8 +49,7 @@
 
   @Override
   public AccessCheckInfo apply(ProjectResource rsrc)
-      throws OrmException, PermissionBackendException, RestApiException, IOException,
-          ConfigInvalidException {
+      throws PermissionBackendException, RestApiException, IOException, ConfigInvalidException {
 
     AccessCheckInput input = new AccessCheckInput();
     input.ref = refName;
diff --git a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
index 3855b78..8cc8298 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedIn;
 import com.google.gerrit.server.project.CommitResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,8 +35,7 @@
   }
 
   @Override
-  public IncludedInInfo apply(CommitResource rsrc)
-      throws RestApiException, OrmException, IOException {
+  public IncludedInInfo apply(CommitResource rsrc) throws RestApiException, IOException {
     RevCommit commit = rsrc.getCommit();
     Project.NameKey project = rsrc.getProjectState().getNameKey();
     return includedIn.apply(project, commit.getId().getName());
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 63dcf5c..25bdadb 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import com.google.common.flogger.FluentLogger;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -22,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.project.CommitResource;
@@ -30,7 +32,6 @@
 import com.google.gerrit.server.project.Reachable;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,14 +40,13 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
 public class CommitsCollection implements ChildCollection<ProjectResource, CommitResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
   private final ChangeIndexCollection indexes;
@@ -107,21 +107,25 @@
   /** @return true if {@code commit} is visible to the caller. */
   public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException {
     Project.NameKey project = state.getNameKey();
-
-    // Look for changes associated with the commit.
-    if (indexes.getSearchIndex() != null) {
-      try {
-        List<ChangeData> changes =
-            queryProvider.get().enforceVisibility(true).byProjectCommit(project, commit);
-        if (!changes.isEmpty()) {
-          return true;
-        }
-      } catch (OrmException e) {
-        logger.atSevere().withCause(e).log(
-            "Cannot look up change for commit %s in %s", commit.name(), project);
-      }
+    if (indexes.getSearchIndex() == null) {
+      // No index in slaves, fall back to scanning refs.
+      return reachable.fromRefs(project, repo, commit, repo.getRefDatabase().getRefs());
     }
 
-    return reachable.fromRefs(project, repo, commit, repo.getRefDatabase().getRefs());
+    // 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 870eb0f..ee3d0a8 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -16,7 +16,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -29,10 +29,10 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.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;
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -93,7 +92,7 @@
   @Override
   public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
       throws PermissionBackendException, AuthException, IOException, ConfigInvalidException,
-          OrmException, InvalidNameException, UpdateException, RestApiException {
+          InvalidNameException, UpdateException, RestApiException {
     PermissionBackend.ForProject forProject =
         permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
     if (!check(forProject, ProjectPermission.READ_CONFIG)) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 3785784..6844cac 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -19,7 +19,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -35,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
@@ -115,7 +115,7 @@
     }
 
     CreateProjectArgs args = new CreateProjectArgs();
-    args.setProjectName(ProjectUtil.stripGitSuffix(name));
+    args.setProjectName(ProjectUtil.sanitizeProjectName(name));
 
     String parentName =
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 0134ce3..3a2bd6f 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,7 +45,7 @@
 
   @Override
   public Response<?> apply(BranchResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (isConfigRef(rsrc.getBranchKey().get())) {
       // Never allow to delete the meta config branch.
       throw new MethodNotAllowedException(
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
index 6e60193..d429655 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -40,7 +39,7 @@
 
   @Override
   public Response<?> apply(ProjectResource project, DeleteBranchesInput input)
-      throws OrmException, IOException, RestApiException, PermissionBackendException {
+      throws IOException, RestApiException, PermissionBackendException {
     if (input == null || input.branches == null || input.branches.isEmpty()) {
       throw new BadRequestException("branches must be specified");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 9a9ead3..8861c0b 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefValidationHelper;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -180,15 +179,13 @@
    * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
    * @param refsToDelete the refs to be deleted.
    * @param prefix the prefix of the refs.
-   * @throws OrmException
    * @throws IOException
    * @throws ResourceConflictException
    * @throws PermissionBackendException
    */
   public void deleteMultipleRefs(
       ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
-      throws OrmException, IOException, ResourceConflictException, PermissionBackendException,
-          AuthException {
+      throws IOException, ResourceConflictException, PermissionBackendException, AuthException {
     if (refsToDelete.isEmpty()) {
       return;
     }
@@ -204,8 +201,7 @@
       ImmutableSet<String> refs =
           prefix == null
               ? refsToDelete
-              : refsToDelete
-                  .stream()
+              : refsToDelete.stream()
                   .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref)
                   .collect(toImmutableSet());
       for (String ref : refs) {
@@ -230,7 +226,7 @@
 
   private ReceiveCommand createDeleteCommand(
       ProjectState projectState, Repository r, String refName)
-      throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
+      throws IOException, ResourceConflictException, PermissionBackendException {
     Ref ref = r.getRefDatabase().getRef(refName);
     ReceiveCommand command;
     if (ref == null) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index f7cce11..33955ee 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.TagResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +40,7 @@
 
   @Override
   public Response<?> apply(TagResource resource, Input input)
-      throws OrmException, RestApiException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref);
 
     if (isConfigRef(tag)) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTags.java b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
index bf2c524..6e8ec37 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTags.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -40,7 +39,7 @@
 
   @Override
   public Response<?> apply(ProjectResource project, DeleteTagsInput input)
-      throws OrmException, RestApiException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (input == null || input.tags == null || input.tags.isEmpty()) {
       throw new BadRequestException("tags must be specified");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index a699e41..23115de 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -55,14 +56,14 @@
   private final boolean canGC;
   private final GarbageCollection.Factory garbageCollectionFactory;
   private final WorkQueue workQueue;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
   GarbageCollect(
       GitRepositoryManager repoManager,
       GarbageCollection.Factory garbageCollectionFactory,
       WorkQueue workQueue,
-      UrlFormatter urlFormatter) {
+      DynamicItem<UrlFormatter> urlFormatter) {
     this.workQueue = workQueue;
     this.urlFormatter = urlFormatter;
     this.canGC = repoManager instanceof LocalDiskRepositoryManager;
@@ -99,7 +100,9 @@
     WorkQueue.Task<Void> task = (WorkQueue.Task<Void>) workQueue.getDefaultQueue().submit(job);
 
     Optional<String> url =
-        urlFormatter.getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
+        urlFormatter
+            .get()
+            .getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
     // We're in a HTTP handler, so must be present.
     checkState(url.isPresent());
     return Response.accepted(url.get());
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 0f46535..744c8b2 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
@@ -196,7 +195,7 @@
           info.local.put(section.getName(), createAccessSection(groups, section));
         }
 
-      } else if (RefConfigSection.isValid(name)) {
+      } else if (AccessSection.isValidRefSectionName(name)) {
         if (check(perm, name, WRITE_CONFIG)) {
           info.local.put(name, createAccessSection(groups, section));
           info.ownerOf.add(name);
@@ -272,9 +271,7 @@
     info.configVisible = canReadConfig || canWriteConfig;
 
     info.groups =
-        groups
-            .entrySet()
-            .stream()
+        groups.entrySet().stream()
             .filter(e -> e.getValue() != null)
             .collect(toMap(e -> e.getKey().get(), Map.Entry::getValue));
 
diff --git a/java/com/google/gerrit/server/restapi/project/Index.java b/java/com/google/gerrit/server/restapi/project/Index.java
index 1b2a523..bc58b23 100644
--- a/java/com/google/gerrit/server/restapi/project/Index.java
+++ b/java/com/google/gerrit/server/restapi/project/Index.java
@@ -16,22 +16,19 @@
 
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.IndexProjectInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,8 +38,6 @@
 @RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
 @Singleton
 public class Index implements RestModifyView<ProjectResource, IndexProjectInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final ProjectIndexer indexer;
   private final ListeningExecutorService executor;
   private final Provider<ListChildProjects> listChildProjectsProvider;
@@ -59,15 +54,12 @@
 
   @Override
   public Response.Accepted apply(ProjectResource rsrc, IndexProjectInput input)
-      throws IOException, AuthException, OrmException, PermissionBackendException,
-          ResourceConflictException {
+      throws IOException, PermissionBackendException, RestApiException {
     String response = "Project " + rsrc.getName() + " submitted for reindexing";
 
     reindex(rsrc.getNameKey(), input.async);
     if (Boolean.TRUE.equals(input.indexChildren)) {
-      ListChildProjects listChildProjects = listChildProjectsProvider.get();
-      listChildProjects.setRecursive(true);
-      for (ProjectInfo child : listChildProjects.apply(rsrc)) {
+      for (ProjectInfo child : listChildProjectsProvider.get().withRecursive(true).apply(rsrc)) {
         reindex(new Project.NameKey(child.name), input.async);
       }
 
@@ -76,18 +68,10 @@
     return Response.accepted(response);
   }
 
-  private void reindex(Project.NameKey project, Boolean async) throws IOException {
+  private void reindex(Project.NameKey project, Boolean async) {
     if (Boolean.TRUE.equals(async)) {
       @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError =
-          executor.submit(
-              () -> {
-                try {
-                  indexer.index(project);
-                } catch (IOException e) {
-                  logger.atWarning().withCause(e).log("reindexing project %s failed", project);
-                }
-              });
+      Future<?> possiblyIgnoredError = executor.submit(() -> indexer.index(project));
     } else {
       indexer.index(project);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 3067c89..3879720 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -17,22 +17,19 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ChildProjects;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import java.util.HashMap;
+import com.google.inject.Provider;
 import java.util.List;
-import java.util.Map;
 import org.kohsuke.args4j.Option;
 
 public class ListChildProjects implements RestReadView<ProjectResource> {
@@ -40,33 +37,42 @@
   @Option(name = "--recursive", usage = "to list child projects recursively")
   private boolean recursive;
 
-  private final ProjectCache projectCache;
+  @Option(name = "--limit", usage = "maximum number of parents projects to list")
+  private int limit;
+
   private final PermissionBackend permissionBackend;
-  private final AllProjectsName allProjects;
-  private final ProjectJson json;
   private final ChildProjects childProjects;
+  private final Provider<QueryProjects> queryProvider;
 
   @Inject
   ListChildProjects(
-      ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      AllProjectsName allProjectsName,
-      ProjectJson json,
-      ChildProjects childProjects) {
-    this.projectCache = projectCache;
+      ChildProjects childProjects,
+      Provider<QueryProjects> queryProvider) {
     this.permissionBackend = permissionBackend;
-    this.allProjects = allProjectsName;
-    this.json = json;
     this.childProjects = childProjects;
+    this.queryProvider = queryProvider;
   }
 
-  public void setRecursive(boolean recursive) {
+  public ListChildProjects withRecursive(boolean recursive) {
     this.recursive = recursive;
+    return this;
+  }
+
+  public ListChildProjects withLimit(int limit) {
+    this.limit = limit;
+    return this;
   }
 
   @Override
   public List<ProjectInfo> apply(ProjectResource rsrc)
-      throws PermissionBackendException, ResourceConflictException {
+      throws PermissionBackendException, RestApiException {
+    if (limit < 0) {
+      throw new BadRequestException("limit must be a positive number");
+    }
+    if (recursive && limit != 0) {
+      throw new ResourceConflictException("recursive and limit options are mutually exclusive");
+    }
     rsrc.getProjectState().checkStatePermitsRead();
     if (recursive) {
       return childProjects.list(rsrc.getNameKey());
@@ -75,23 +81,14 @@
     return directChildProjects(rsrc.getNameKey());
   }
 
-  private List<ProjectInfo> directChildProjects(Project.NameKey parent)
-      throws PermissionBackendException {
-    Map<Project.NameKey, Project> children = new HashMap<>();
-    for (Project.NameKey name : projectCache.all()) {
-      ProjectState c = projectCache.get(name);
-      if (c != null
-          && parent.equals(c.getProject().getParent(allProjects))
-          && c.statePermitsRead()) {
-        children.put(c.getNameKey(), c.getProject());
-      }
-    }
-    return permissionBackend
-        .currentUser()
-        .filter(ProjectPermission.ACCESS, children.keySet())
-        .stream()
-        .sorted()
-        .map((p) -> json.format(children.get(p)))
+  private List<ProjectInfo> directChildProjects(Project.NameKey parent) throws RestApiException {
+    PermissionBackend.WithUser currentUser = permissionBackend.currentUser();
+    return queryProvider.get().withQuery("parent:" + parent.get()).withLimit(limit).apply().stream()
+        .filter(
+            p ->
+                currentUser
+                    .project(new Project.NameKey(p.name))
+                    .testOrFalse(ProjectPermission.ACCESS))
         .collect(toList());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index ef5abcb..4f3dbb7 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,23 +14,27 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Ordering.natural;
 import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Strings;
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 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.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -41,6 +45,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.ioutil.RegexListSearcher;
@@ -54,6 +59,7 @@
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -62,7 +68,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -70,12 +75,15 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -97,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 {
@@ -257,6 +254,8 @@
   private String matchSubstring;
   private String matchRegex;
   private AccountGroup.UUID groupUuid;
+  private final Provider<QueryProjects> queryProjectsProvider;
+  private final boolean listProjectsFromIndex;
 
   @Inject
   protected ListProjects(
@@ -267,7 +266,9 @@
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       ProjectNode.Factory projectNodeFactory,
-      WebLinks webLinks) {
+      WebLinks webLinks,
+      Provider<QueryProjects> queryProjectsProvider,
+      @GerritServerConfig Config config) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.groupResolver = groupResolver;
@@ -276,6 +277,8 @@
     this.permissionBackend = permissionBackend;
     this.projectNodeFactory = projectNodeFactory;
     this.webLinks = webLinks;
+    this.queryProjectsProvider = queryProjectsProvider;
+    this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
   }
 
   public List<String> getShowBranch() {
@@ -304,7 +307,7 @@
       throws BadRequestException, PermissionBackendException {
     if (format == OutputFormat.TEXT) {
       ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      display(buf);
+      displayToStream(buf);
       return BinaryResult.create(buf.toByteArray())
           .setContentType("text/plain")
           .setCharacterEncoding(UTF_8);
@@ -314,11 +317,100 @@
 
   public SortedMap<String, ProjectInfo> apply()
       throws BadRequestException, PermissionBackendException {
+    Optional<String> projectQuery = expressAsProjectsQuery();
+    if (projectQuery.isPresent()) {
+      return applyAsQuery(projectQuery.get());
+    }
+
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public SortedMap<String, ProjectInfo> display(@Nullable OutputStream displayOutputStream)
+  private Optional<String> expressAsProjectsQuery() {
+    return listProjectsFromIndex
+            && !all
+            && state != HIDDEN
+            && isNullOrEmpty(matchPrefix)
+            && isNullOrEmpty(matchRegex)
+            && isNullOrEmpty(matchSubstring) // TODO: see Issue 10446
+            && type == FilterType.ALL
+            && showBranch.isEmpty()
+            && !showTree
+        ? Optional.of(stateToQuery())
+        : Optional.empty();
+  }
+
+  private String stateToQuery() {
+    List<String> queries = new ArrayList<>();
+    if (state == null) {
+      queries.add("(state:active OR state:read-only)");
+    } else {
+      queries.add(String.format("(state:%s)", state.name()));
+    }
+
+    return Joiner.on(" AND ").join(queries).toString();
+  }
+
+  private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
+    try {
+      return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
+          .stream()
+          .collect(
+              ImmutableSortedMap.toImmutableSortedMap(
+                  natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
+    } catch (StorageException | MethodNotAllowedException e) {
+      logger.atWarning().withCause(e).log(
+          "Internal error while processing the query '%s' request", query);
+      throw new BadRequestException("Internal error while processing the query request");
+    }
+  }
+
+  private ProjectInfo nullifyDescription(ProjectInfo p) {
+    p.description = null;
+    return p;
+  }
+
+  private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
+    try {
+      if (format.isJson()) {
+        format.newGson().toJson(applyAsQuery(query), out);
+      } else {
+        newProjectsNamesStream(query).forEach(out::println);
+      }
+      out.flush();
+    } catch (StorageException | MethodNotAllowedException e) {
+      logger.atWarning().withCause(e).log(
+          "Internal error while processing the query '%s' request", query);
+      throw new BadRequestException("Internal error while processing the query request");
+    }
+  }
+
+  private Stream<String> newProjectsNamesStream(String query)
+      throws MethodNotAllowedException, BadRequestException {
+    Stream<String> projects =
+        queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
+    if (limit > 0) {
+      projects = projects.limit(limit);
+    }
+
+    return projects;
+  }
+
+  public void displayToStream(OutputStream displayOutputStream)
+      throws BadRequestException, PermissionBackendException {
+    PrintWriter stdout =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
+    Optional<String> projectsQuery = expressAsProjectsQuery();
+
+    if (projectsQuery.isPresent()) {
+      printQueryResults(projectsQuery.get(), stdout);
+    } else {
+      display(stdout);
+    }
+  }
+
+  @Nullable
+  public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
       throws BadRequestException, PermissionBackendException {
     if (all && state != null) {
       throw new BadRequestException("'all' and 'state' may not be used together");
@@ -333,17 +425,6 @@
       }
     }
 
-    PrintWriter stdout = null;
-    if (displayOutputStream != null) {
-      stdout =
-          new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
-    }
-
-    if (type == FilterType.PARENT_CANDIDATES) {
-      // Historically, PARENT_CANDIDATES implied showDescription.
-      showDescription = true;
-    }
-
     int foundIndex = 0;
     int found = 0;
     TreeMap<String, ProjectInfo> output = new TreeMap<>();
@@ -352,9 +433,10 @@
     PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
     try {
-      for (Project.NameKey projectName : filter(perm)) {
-        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.
@@ -371,30 +453,26 @@
           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) {
-          info.description = Strings.emptyToNull(e.getProject().getDescription());
+          info.description = emptyToNull(e.getProject().getDescription());
         }
         info.state = e.getProject().getState();
 
@@ -405,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)) {
@@ -447,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);
@@ -465,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);
 
@@ -506,55 +547,79 @@
     }
   }
 
-  private Collection<Project.NameKey> filter(PermissionBackend.WithUser perm)
-      throws BadRequestException, PermissionBackendException {
-    Stream<Project.NameKey> matches = scan();
-    if (type == FilterType.PARENT_CANDIDATES) {
-      matches = parentsOf(matches);
+  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(' ');
     }
+  }
 
-    List<Project.NameKey> results = new ArrayList<>();
-    List<Project.NameKey> projectNameKeys = matches.sorted().collect(toList());
-    for (Project.NameKey nameKey : projectNameKeys) {
-      ProjectState state = projectCache.get(nameKey);
-      requireNonNull(state, () -> String.format("Failed to load project %s", nameKey));
+  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());
+      }
+    }
+  }
 
-      // 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;
+  private List<Ref> retieveBranchRefs(ProjectState e) throws PermissionBackendException {
+    boolean canReadAllRefs = e.statePermitsRead();
+    if (canReadAllRefs) {
       try {
-        perm.project(nameKey).check(permissionToCheck);
-        results.add(nameKey);
-      } catch (AuthException e) {
-        // Not added to results.
+        permissionBackend.user(currentUser).project(e.getNameKey()).check(ProjectPermission.READ);
+      } catch (AuthException exp) {
+        canReadAllRefs = false;
       }
     }
 
-    return results;
+    return getBranchRefs(e.getNameKey(), canReadAllRefs);
   }
 
-  private Stream<Project.NameKey> parentsOf(Stream<Project.NameKey> matches) {
-    return matches
-        .map(
-            p -> {
-              ProjectState ps = projectCache.get(p);
-              if (ps != null) {
-                Project.NameKey parent = ps.getProject().getParent();
-                if (parent != null) {
-                  if (projectCache.get(parent) != null) {
-                    return parent;
-                  }
-                  logger.atWarning().log(
-                      "parent project %s of project %s not found", parent.get(), ps.getName());
-                }
-              }
-              return null;
-            })
+  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)
-        .distinct();
+        .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(
@@ -587,9 +652,7 @@
       return projectCache.byName(matchPrefix).stream();
     } else if (matchSubstring != null) {
       checkMatchOptions(matchPrefix == null && matchRegex == null);
-      return projectCache
-          .all()
-          .stream()
+      return projectCache.all().stream()
           .filter(
               p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
     } else if (matchRegex != null) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 7a46b9c..98c8c61 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -226,9 +226,6 @@
     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/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index e06b406..31c90e5 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -42,7 +43,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.eclipse.jgit.lib.Constants;
 
 @Singleton
 public class ProjectsCollection
@@ -136,9 +136,7 @@
   @Nullable
   private ProjectResource _parse(String id, boolean checkAccess)
       throws IOException, PermissionBackendException, ResourceConflictException {
-    if (id.endsWith(Constants.DOT_GIT_EXT)) {
-      id = id.substring(0, id.length() - Constants.DOT_GIT_EXT.length());
-    }
+    id = ProjectUtil.sanitizeProjectName(id);
 
     Project.NameKey nameKey = new Project.NameKey(id);
     ProjectState state = projectCache.checkedGet(nameKey);
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index 921a591..150989b 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -55,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;
@@ -192,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/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 1e094a0..8727df3 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.query.project.ProjectQueryBuilder;
 import com.google.gerrit.server.query.project.ProjectQueryProcessor;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
@@ -49,8 +48,9 @@
       name = "--query",
       aliases = {"-q"},
       usage = "project query")
-  public void setQuery(String query) {
+  public QueryProjects withQuery(String query) {
     this.query = query;
+    return this;
   }
 
   @Option(
@@ -58,8 +58,9 @@
       aliases = {"-n"},
       metaVar = "CNT",
       usage = "maximum number of projects to list")
-  public void setLimit(int limit) {
+  public QueryProjects withLimit(int limit) {
     this.limit = limit;
+    return this;
   }
 
   @Option(
@@ -67,8 +68,9 @@
       aliases = {"-S"},
       metaVar = "CNT",
       usage = "number of projects to skip")
-  public void setStart(int start) {
+  public QueryProjects withStart(int start) {
     this.start = start;
+    return this;
   }
 
   @Inject
@@ -85,7 +87,11 @@
 
   @Override
   public List<ProjectInfo> apply(TopLevelResource resource)
-      throws BadRequestException, MethodNotAllowedException, OrmException {
+      throws BadRequestException, MethodNotAllowedException {
+    return apply();
+  }
+
+  public List<ProjectInfo> apply() throws BadRequestException, MethodNotAllowedException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java b/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
index 2a2fc866..9f686eb 100644
--- a/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
+++ b/java/com/google/gerrit/server/restapi/project/RepositoryStatistics.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.CaseFormat;
-import java.util.Map.Entry;
+import java.util.Map;
 import java.util.Properties;
 import java.util.TreeMap;
 
@@ -23,7 +23,7 @@
   private static final long serialVersionUID = 1L;
 
   RepositoryStatistics(Properties p) {
-    for (Entry<Object, Object> e : p.entrySet()) {
+    for (Map.Entry<Object, Object> e : p.entrySet()) {
       put(
           CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getKey().toString()),
           e.getValue());
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 19e89a9..5277cf8 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -17,7 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -83,8 +82,7 @@
   @Override
   public ProjectAccessInfo apply(ProjectResource rsrc, ProjectAccessInput input)
       throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException,
-          BadRequestException, UnprocessableEntityException, OrmException,
-          PermissionBackendException {
+          BadRequestException, UnprocessableEntityException, PermissionBackendException {
     MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
 
     ProjectConfig config;
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index c8857a2..e206319 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
@@ -146,7 +146,7 @@
       boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
 
       if (!isGlobalCapabilities) {
-        if (!AccessSection.isValid(name)) {
+        if (!AccessSection.isValidRefSectionName(name)) {
           throw new BadRequestException("invalid section name");
         }
         RefPattern.validate(name);
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
index 65ac88f..2be6c19 100644
--- a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -80,7 +80,7 @@
     try {
       labelTypes = cd.getLabelTypes().getLabelTypes();
       approvals = cd.currentApprovals();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log(
           "Unable to fetch labels and approvals for change %s", cd.getId());
 
@@ -124,8 +124,7 @@
 
   private static List<PatchSetApproval> getApprovalsForLabel(
       List<PatchSetApproval> approvals, LabelType t) {
-    return approvals
-        .stream()
+    return approvals.stream()
         .filter(input -> input.getLabel().equals(t.getLabelId().get()))
         .collect(toImmutableList());
   }
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index b9ddbc6..592c269c 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -23,13 +23,13 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -66,12 +66,12 @@
     try {
       labelTypes = cd.getLabelTypes().getLabelTypes();
       approvals = cd.currentApprovals();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_LABELS);
       return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
     }
 
-    boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(l -> l.ignoreSelfApproval());
+    boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(LabelType::ignoreSelfApproval);
     if (!shouldIgnoreSelfApproval) {
       // Shortcut to avoid further processing if no label should ignore uploader approvals
       return ImmutableList.of();
@@ -80,7 +80,7 @@
     Account.Id uploader;
     try {
       uploader = cd.currentPatchSet().getUploader();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_UPLOADER);
       return singletonRuleError(E_UNABLE_TO_FETCH_UPLOADER);
     }
@@ -154,8 +154,7 @@
   @VisibleForTesting
   static Collection<PatchSetApproval> filterOutPositiveApprovalsOfUser(
       Collection<PatchSetApproval> approvals, Account.Id user) {
-    return approvals
-        .stream()
+    return approvals.stream()
         .filter(input -> input.getValue() < 0 || !input.getAccountId().equals(user))
         .collect(toImmutableList());
   }
@@ -163,8 +162,7 @@
   @VisibleForTesting
   static Collection<PatchSetApproval> filterApprovalsByLabel(
       Collection<PatchSetApproval> approvals, LabelType t) {
-    return approvals
-        .stream()
+    return approvals.stream()
         .filter(input -> input.getLabelId().get().equals(t.getLabelId().get()))
         .collect(toImmutableList());
   }
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 412e0f9..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;
@@ -82,7 +83,9 @@
   @Override
   public void setPredicate(Predicate goal) {
     super.setPredicate(goal);
-    setReductionLimit(args.reductionLimit(goal));
+    int reductionLimit = args.reductionLimit(goal);
+    logger.atFine().log("setting reductionLimit %d", reductionLimit);
+    setReductionLimit(reductionLimit);
   }
 
   /**
@@ -170,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;
@@ -184,6 +188,7 @@
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         GitRepositoryManager repositoryManager,
+        PluginConfigFactory pluginConfigFactory,
         PatchListCache patchListCache,
         PatchSetInfoFactory patchSetInfoFactory,
         IdentifiedUser.GenericFactory userFactory,
@@ -194,6 +199,7 @@
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.repositoryManager = repositoryManager;
+      this.pluginConfigFactory = pluginConfigFactory;
       this.patchListCache = patchListCache;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.userFactory = userFactory;
@@ -211,6 +217,8 @@
               "compileReductionLimit",
               (int) Math.min(10L * limit, Integer.MAX_VALUE));
       compileLimit = limit <= 0 ? Integer.MAX_VALUE : limit;
+
+      logger.atInfo().log("reductionLimit: %d, compileLimit: %d", reductionLimit, compileLimit);
     }
 
     private int reductionLimit(Predicate goal) {
@@ -232,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 eb188db..9cde54c 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -36,7 +37,6 @@
 import com.google.gerrit.server.project.RuleEvalException;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -149,17 +149,17 @@
     try {
       change = cd.change();
       if (change == null) {
-        throw new OrmException("No change found");
+        throw new StorageException("No change found");
       }
 
       if (projectState == null) {
         throw new NoSuchProjectException(cd.project());
       }
-    } catch (OrmException | NoSuchProjectException e) {
+    } catch (StorageException | NoSuchProjectException e) {
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if (!opts.allowClosed() && change.getStatus().isClosed()) {
+    if (!opts.allowClosed() && change.isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index aa529d7..9bae091 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.rules.StoredValue.create;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,6 +28,7 @@
 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;
@@ -35,7 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.exceptions.SystemException;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import java.io.IOException;
@@ -56,7 +57,7 @@
     ChangeData cd = CHANGE_DATA.get(engine);
     try {
       return cd.change();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new SystemException("Cannot load change " + cd.getId());
     }
   }
@@ -65,7 +66,7 @@
     ChangeData cd = CHANGE_DATA.get(engine);
     try {
       return cd.currentPatchSet();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new SystemException(e.getMessage());
     }
   }
@@ -120,6 +121,15 @@
         }
       };
 
+  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/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 0f1caa8..9446b7c 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -22,38 +22,27 @@
 import static com.google.gerrit.server.schema.AclUtil.grant;
 import static com.google.gerrit.server.schema.AclUtil.rule;
 
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.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.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;
@@ -77,13 +66,6 @@
   private final GroupReference registered;
   private final GroupReference owners;
 
-  @Nullable private GroupReference admin;
-  @Nullable private GroupReference batch;
-  private String message;
-  private int firstChangeId = Sequences.FIRST_CHANGE_ID;
-  private LabelType codeReviewLabel;
-  private List<LabelType> additionalLabelType;
-
   @Inject
   AllProjectsCreator(
       GitRepositoryManager repositoryManager,
@@ -101,57 +83,16 @@
     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, OrmException {
+  public void create(AllProjectsInput input) throws IOException, ConfigInvalidException {
     try (Repository git = repositoryManager.openRepository(allProjectsName)) {
-      initAllProjects(git);
+      initAllProjects(git, input);
     } catch (RepositoryNotFoundException notFound) {
       // A repository may be missing if this project existed only to store
       // inheritable permissions. For example 'All-Projects'.
       try (Repository git = repositoryManager.createRepository(allProjectsName)) {
-        initAllProjects(git);
+        initAllProjects(git, input);
         RefUpdate u = git.updateRef(Constants.HEAD);
         u.link(RefNames.REFS_CONFIG);
       } catch (RepositoryNotFoundException err) {
@@ -161,76 +102,44 @@
     }
   }
 
-  private void initAllProjects(Repository git)
-      throws IOException, ConfigInvalidException, OrmException {
+  private void initAllProjects(Repository git, AllProjectsInput input)
+      throws ConfigInvalidException, IOException {
     BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
     try (MetaDataUpdate md =
         new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git, bru)) {
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
       md.setMessage(
-          MoreObjects.firstNonNull(
-              Strings.emptyToNull(message),
-              "Initialized Gerrit Code Review " + Version.getVersion()));
+          input.commitMessage().isPresent()
+              ? input.commitMessage().get()
+              : "Initialized Gerrit Code Review " + Version.getVersion());
 
+      // 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();
@@ -239,28 +148,84 @@
     }
   }
 
-  @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 {
+  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.
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 3b1124d..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,7 +29,6 @@
 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;
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index 93cad19..6a97954 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -7,8 +7,10 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
@@ -16,7 +18,6 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/org/eclipse/jgit:server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:dbcp",
diff --git a/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
index 5146140..3f74cda 100644
--- a/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,17 +36,17 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 23001: // UNIQUE CONSTRAINT VIOLATION
       case 23505: // DUPLICATE_KEY_1
-        return new OrmDuplicateKeyException("account_patch_reviews", err);
+        return new DuplicateKeyException("account_patch_reviews", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on account_patch_reviews", err);
+        return new StorageException(op + " failure on account_patch_reviews", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index fa4de40..1b48b2c 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -21,6 +21,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -31,8 +33,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.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -167,7 +167,7 @@
   public void start() {
     try {
       createTableIfNotExists();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to create table to store account patch reviews");
     }
   }
@@ -176,7 +176,7 @@
     return ds.getConnection();
   }
 
-  public void createTableIfNotExists() throws OrmException {
+  public void createTableIfNotExists() {
     try (Connection con = ds.getConnection();
         Statement stmt = con.createStatement()) {
       doCreateTable(stmt);
@@ -197,7 +197,7 @@
             + ")");
   }
 
-  public void dropTableIfExists() throws OrmException {
+  public void dropTableIfExists() {
     try (Connection con = ds.getConnection();
         Statement stmt = con.createStatement()) {
       stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews");
@@ -210,8 +210,7 @@
   public void stop() {}
 
   @Override
-  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
+  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -225,8 +224,8 @@
       stmt.executeUpdate();
       return true;
     } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
+      StorageException ormException = convertError("insert", e);
+      if (ormException instanceof DuplicateKeyException) {
         return false;
       }
       throw ormException;
@@ -234,8 +233,7 @@
   }
 
   @Override
-  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
-      throws OrmException {
+  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths) {
     if (paths == null || paths.isEmpty()) {
       return;
     }
@@ -255,8 +253,8 @@
       }
       stmt.executeBatch();
     } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
+      StorageException ormException = convertError("insert", e);
+      if (ormException instanceof DuplicateKeyException) {
         return;
       }
       throw ormException;
@@ -264,8 +262,7 @@
   }
 
   @Override
-  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
+  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -283,7 +280,7 @@
   }
 
   @Override
-  public void clearReviewed(PatchSet.Id psId) throws OrmException {
+  public void clearReviewed(PatchSet.Id psId) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -298,8 +295,7 @@
   }
 
   @Override
-  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
-      throws OrmException {
+  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
@@ -331,11 +327,11 @@
     }
   }
 
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     if (err.getCause() == null && err.getNextException() != null) {
       err.initCause(err.getNextException());
     }
-    return new OrmException(op + " failure on account_patch_reviews", err);
+    return new StorageException(op + " failure on account_patch_reviews", err);
   }
 
   private static String getSQLState(SQLException err) {
diff --git a/java/com/google/gerrit/server/schema/JdbcUtil.java b/java/com/google/gerrit/server/schema/JdbcUtil.java
index 3995339..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 {
 
diff --git a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
index aa05a08..b0a3370 100644
--- a/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MariaDBAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,18 +36,18 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 1022: // ER_DUP_KEY
       case 1062: // ER_DUP_ENTRY
       case 1169: // ER_DUP_UNIQUE;
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
index d648ed0..35bf2cb 100644
--- a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -37,18 +37,18 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 1022: // ER_DUP_KEY
       case 1062: // ER_DUP_ENTRY
       case 1169: // ER_DUP_UNIQUE;
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       default:
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
index b31710e..3d08a73 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
@@ -20,12 +20,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.stream.IntStream;
@@ -77,7 +77,7 @@
     this.schemaVersions = schemaVersions;
   }
 
-  public void update(UpdateUI ui) throws OrmException {
+  public void update(UpdateUI ui) {
     ensureSchemaCreated();
 
     int currentVersion = versionManager.read();
@@ -91,20 +91,20 @@
     for (int nextVersion : requiredUpgrades(currentVersion, schemaVersions.keySet())) {
       try {
         ui.message(String.format("Migrating data to schema %d ...", nextVersion));
-        NoteDbSchemaVersions.get(schemaVersions, nextVersion, args).upgrade(ui);
+        NoteDbSchemaVersions.get(schemaVersions, nextVersion).upgrade(args, ui);
         versionManager.increment(nextVersion - 1);
       } catch (Exception e) {
-        throw new OrmException(
+        throw new StorageException(
             String.format("Failed to upgrade to schema version %d", nextVersion), e);
       }
     }
   }
 
-  private void ensureSchemaCreated() throws OrmException {
+  private void ensureSchemaCreated() {
     try {
       schemaCreator.ensureCreated();
     } catch (IOException | ConfigInvalidException e) {
-      throw new OrmException("Cannot initialize Gerrit site");
+      throw new StorageException("Cannot initialize Gerrit site");
     }
   }
 
@@ -114,7 +114,7 @@
     NOTE_DB
   }
 
-  private void checkNoteDbConfigFor216() throws OrmException {
+  private void checkNoteDbConfigFor216() {
     // Check that the NoteDb migration config matches what we expect from a site that both:
     // * Completed the change migration to NoteDB.
     // * Ran schema upgrades from a 2.16 final release.
@@ -125,7 +125,7 @@
                 "noteDb", "changes", "primaryStorage", PrimaryStorageFor216Compatibility.REVIEW_DB)
             != PrimaryStorageFor216Compatibility.NOTE_DB
         || !cfg.getBoolean("noteDb", "changes", "disableReviewDb", false)) {
-      throw new OrmException(
+      throw new StorageException(
           "You appear to be upgrading from a 2.x site, but the NoteDb change migration was"
               + " not completed. See documentation:\n"
               + "https://gerrit-review.googlesource.com/Documentation/note-db.html#migration");
@@ -149,24 +149,24 @@
     //    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(
+        throw new StorageException(
             "You appear to be upgrading to 3.x from a version prior to 2.16; you must upgrade to"
                 + " 2.16.x first");
       }
     } catch (IOException e) {
-      throw new OrmException("Failed to check NoteDb migration state", e);
+      throw new StorageException("Failed to check NoteDb migration state", e);
     }
   }
 
   @VisibleForTesting
   static ImmutableList<Integer> requiredUpgrades(
-      int currentVersion, ImmutableSortedSet<Integer> allVersions) throws OrmException {
+      int currentVersion, ImmutableSortedSet<Integer> allVersions) {
     int firstVersion = allVersions.first();
     int latestVersion = allVersions.last();
     if (currentVersion == latestVersion) {
       return ImmutableList.of();
     } else if (currentVersion > latestVersion) {
-      throw new OrmException(
+      throw new StorageException(
           String.format(
               "Cannot downgrade NoteDb schema from version %d to %d",
               currentVersion, latestVersion));
@@ -178,7 +178,7 @@
       firstUpgradeVersion = firstVersion;
     } else {
       if (currentVersion < firstVersion - 1) {
-        throw new OrmException(
+        throw new StorageException(
             String.format(
                 "Cannot skip NoteDb schema from version %d to %d", currentVersion, firstVersion));
       }
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
index e90b2b8..b6a7a1c 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -22,22 +23,24 @@
 /**
  * Schema upgrade implementation.
  *
- * <p>Implementations must define a single public constructor that takes an {@link Arguments}. The
- * recommended idiom is to pull out whichever individual fields from the {@code Arguments} are
- * required by this implementation.
+ * <p>Implementations must have a single non-private constructor with no arguments (e.g. the default
+ * constructor).
  */
 interface NoteDbSchemaVersion {
   @Singleton
   class Arguments {
     final GitRepositoryManager repoManager;
     final AllProjectsName allProjects;
+    final AllUsersName allUsers;
 
     @Inject
-    Arguments(GitRepositoryManager repoManager, AllProjectsName allProjects) {
+    Arguments(
+        GitRepositoryManager repoManager, AllProjectsName allProjects, AllUsersName allUsers) {
       this.repoManager = repoManager;
       this.allProjects = allProjects;
+      this.allUsers = allUsers;
     }
   }
 
-  void upgrade(UpdateUI ui) throws Exception;
+  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
index 63bffec..33534fc 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.ProvisionException;
@@ -65,7 +65,7 @@
                 "Unsupported schema version %d; expected schema version %d. %s",
                 current, expected, advice));
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new ProvisionException("Failed to read NoteDb schema version", e);
     }
   }
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
index 5be98d2..7ff0ea6 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionManager.java
@@ -17,14 +17,14 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.IntBlob;
-import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
-import javax.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -45,20 +45,20 @@
     this.repoManager = repoManager;
   }
 
-  public int read() throws OrmException {
+  public int read() {
     try (Repository repo = repoManager.openRepository(allProjectsName)) {
       return IntBlob.parse(repo, REFS_VERSION).map(IntBlob::value).orElse(0);
     } catch (IOException e) {
-      throw new OrmException("Failed to read " + REFS_VERSION, e);
+      throw new StorageException("Failed to read " + REFS_VERSION, e);
     }
   }
 
-  public void init() throws IOException, OrmException {
+  public void init() throws IOException {
     try (Repository repo = repoManager.openRepository(allProjectsName);
         RevWalk rw = new RevWalk(repo)) {
       Optional<IntBlob> old = IntBlob.parse(repo, REFS_VERSION, rw);
       if (old.isPresent()) {
-        throw new OrmException(
+        throw new StorageException(
             String.format(
                 "Expected no old version for %s, found %s", REFS_VERSION, old.get().value()));
       }
@@ -73,12 +73,12 @@
     }
   }
 
-  public void increment(int expectedOldVersion) throws IOException, OrmException {
+  public void increment(int expectedOldVersion) throws IOException {
     try (Repository repo = repoManager.openRepository(allProjectsName);
         RevWalk rw = new RevWalk(repo)) {
       Optional<IntBlob> old = IntBlob.parse(repo, REFS_VERSION, rw);
       if (old.isPresent() && old.get().value() != expectedOldVersion) {
-        throw new OrmException(
+        throw new StorageException(
             String.format(
                 "Expected old version %d for %s, found %d",
                 expectedOldVersion, REFS_VERSION, old.get().value()));
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
index ffd4760..02250f2 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
@@ -20,7 +20,7 @@
 
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.server.UsedAt;
+import com.google.gerrit.common.UsedAt;
 import java.lang.reflect.InvocationTargetException;
 import java.util.Optional;
 import java.util.stream.Stream;
@@ -28,7 +28,7 @@
 public class NoteDbSchemaVersions {
   static final ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> ALL =
       // List all supported NoteDb schema versions here.
-      Stream.of(Schema_180.class)
+      Stream.of(Schema_180.class, Schema_181.class)
           .collect(toImmutableSortedMap(naturalOrder(), v -> guessVersion(v).get(), v -> v));
 
   public static final int FIRST = ALL.firstKey();
@@ -45,13 +45,11 @@
   }
 
   public static NoteDbSchemaVersion get(
-      ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions,
-      int i,
-      NoteDbSchemaVersion.Arguments args) {
+      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(NoteDbSchemaVersion.Arguments.class).newInstance(args);
+      return clazz.getDeclaredConstructor().newInstance();
     } catch (InstantiationException
         | IllegalAccessException
         | NoSuchMethodException
diff --git a/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
index 34f7dba..db68f2e 100644
--- a/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.SQLException;
@@ -36,10 +36,10 @@
   }
 
   @Override
-  public OrmException convertError(String op, SQLException err) {
+  public StorageException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 23505: // DUPLICATE_KEY_1
-        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+        return new DuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
 
       case 23514: // CHECK CONSTRAINT VIOLATION
       case 23503: // FOREIGN KEY CONSTRAINT VIOLATION
@@ -49,7 +49,7 @@
         if (err.getCause() == null && err.getNextException() != null) {
           err.initCause(err.getNextException());
         }
-        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+        return new StorageException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index 483e363..9e12807 100644
--- a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
+++ b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -20,13 +20,13 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
@@ -115,7 +115,7 @@
     return true;
   }
 
-  public void save(PersonIdent personIdent, String commitMessage) throws OrmException {
+  public void save(PersonIdent personIdent, String commitMessage) {
     if (!updated) {
       return;
     }
@@ -126,7 +126,7 @@
     try {
       commit(update);
     } catch (IOException e) {
-      throw new OrmException(e);
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index 8cf8fe7..b78ce73 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -26,20 +25,18 @@
    *
    * <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;
+  void create() throws IOException, ConfigInvalidException;
 
   /**
    * 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;
+  void ensureCreated() throws IOException, ConfigInvalidException;
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
index 0e854d6..0a5823a 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -17,11 +17,11 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -36,8 +36,7 @@
 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.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -90,11 +89,13 @@
   }
 
   @Override
-  public void create() throws OrmException, IOException, ConfigInvalidException {
+  public void create() throws IOException, ConfigInvalidException {
     GroupReference admins = createGroupReference("Administrators");
     GroupReference batchUsers = createGroupReference("Non-Interactive Users");
 
-    allProjectsCreator.setAdministrators(admins).setBatchUsers(batchUsers).create();
+    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();
 
@@ -115,7 +116,7 @@
   }
 
   @Override
-  public void ensureCreated() throws OrmException, IOException, ConfigInvalidException {
+  public void ensureCreated() throws IOException, ConfigInvalidException {
     try {
       repoManager.openRepository(allProjectsName).close();
     } catch (RepositoryNotFoundException e) {
@@ -125,7 +126,7 @@
 
   private void createAdminsGroup(
       Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setDescription("Gerrit Site Administrators").build();
@@ -138,7 +139,7 @@
       Repository allUsersRepo,
       GroupReference groupReference,
       AccountGroup.UUID adminsGroupUuid)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
@@ -151,14 +152,14 @@
 
   private void createGroup(
       Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException, ConfigInvalidException, IOException {
+      throws ConfigInvalidException, IOException {
     InternalGroup createdGroup = createGroupInNoteDb(allUsersRepo, groupCreation, groupUpdate);
     index(createdGroup);
   }
 
   private InternalGroup createGroupInNoteDb(
       Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws ConfigInvalidException, IOException, OrmDuplicateKeyException {
+      throws ConfigInvalidException, IOException, DuplicateKeyException {
     // This method is only executed on a new server which doesn't have any accounts or groups.
     AuditLogFormatter auditLogFormatter =
         AuditLogFormatter.createBackedBy(ImmutableSet.of(), ImmutableSet.of(), serverId);
@@ -203,7 +204,7 @@
     return metaDataUpdate;
   }
 
-  private void index(InternalGroup group) throws IOException {
+  private void index(InternalGroup group) {
     for (GroupIndex groupIndex : indexCollection.getWriteIndexes()) {
       groupIndex.replace(group);
     }
@@ -214,8 +215,7 @@
     return new GroupReference(groupUuid, name);
   }
 
-  private InternalGroupCreation getGroupCreation(Sequences seqs, GroupReference groupReference)
-      throws OrmException {
+  private InternalGroupCreation getGroupCreation(Sequences seqs, GroupReference groupReference) {
     int next = seqs.nextGroupId();
     return InternalGroupCreation.builder()
         .setNameKey(new AccountGroup.NameKey(groupReference.getName()))
diff --git a/java/com/google/gerrit/server/schema/Schema_180.java b/java/com/google/gerrit/server/schema/Schema_180.java
index 4d16022..6912b3e 100644
--- a/java/com/google/gerrit/server/schema/Schema_180.java
+++ b/java/com/google/gerrit/server/schema/Schema_180.java
@@ -15,13 +15,8 @@
 package com.google.gerrit.server.schema;
 
 public class Schema_180 implements NoteDbSchemaVersion {
-  @SuppressWarnings("unused")
-  Schema_180(Arguments args) {
-    // Do nothing.
-  }
-
   @Override
-  public void upgrade(UpdateUI ui) {
+  public void upgrade(Arguments args, UpdateUI ui) {
     // Do nothing; only used to populate the version ref, which is done by the caller.
   }
 }
diff --git a/java/com/google/gerrit/server/schema/Schema_181.java b/java/com/google/gerrit/server/schema/Schema_181.java
new file mode 100644
index 0000000..3054ad3
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_181.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.gpg.PublicKeyStore;
+import org.eclipse.jgit.lib.Repository;
+
+public class Schema_181 implements NoteDbSchemaVersion {
+  @Override
+  public void upgrade(Arguments args, UpdateUI ui) throws Exception {
+    ui.message("Rebuild GPGP note map to build subkey to master key map");
+    try (Repository repo = args.repoManager.openRepository(args.allUsers);
+        PublicKeyStore store = new PublicKeyStore(repo)) {
+      store.rebuildSubkeyMasterKeyMap();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/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/ssh/NoSshKeyCache.java b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 387242c..74bb50c 100644
--- a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.inject.AbstractModule;
diff --git a/java/com/google/gerrit/server/ssh/SshKeyCreator.java b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
index 55ba5ed..beaf1ba 100644
--- a/java/com/google/gerrit/server/ssh/SshKeyCreator.java
+++ b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 
diff --git a/java/com/google/gerrit/server/submit/ChangeSet.java b/java/com/google/gerrit/server/submit/ChangeSet.java
index 422e1b9..e721be41ab 100644
--- a/java/com/google/gerrit/server/submit/ChangeSet.java
+++ b/java/com/google/gerrit/server/submit/ChangeSet.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -77,7 +76,7 @@
     return changeData;
   }
 
-  public ListMultimap<Branch.NameKey, ChangeData> changesByBranch() throws OrmException {
+  public ListMultimap<Branch.NameKey, ChangeData> changesByBranch() {
     ListMultimap<Branch.NameKey, ChangeData> ret =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (ChangeData cd : changeData.values()) {
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index f50525c..8ff3cc5 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -91,7 +90,7 @@
 
     @Override
     protected void updateRepoImpl(RepoContext ctx)
-        throws IntegrationException, IOException, OrmException, MethodNotAllowedException {
+        throws IntegrationException, IOException, MethodNotAllowedException {
       // If there is only one parent, a cherry-pick can be done by taking the
       // delta relative to that one parent and redoing that on the current merge
       // tip.
@@ -146,8 +145,7 @@
     }
 
     @Override
-    public PatchSet updateChangeImpl(ChangeContext ctx)
-        throws OrmException, NoSuchChangeException, IOException {
+    public PatchSet updateChangeImpl(ChangeContext ctx) throws NoSuchChangeException, IOException {
       if (newCommit == null && toMerge.getStatusCode() == SKIPPED_IDENTICAL_TREE) {
         return null;
       }
diff --git a/java/com/google/gerrit/server/submit/CommitMergeStatus.java b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
index 3c7b986..12172dd 100644
--- a/java/com/google/gerrit/server/submit/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
@@ -16,10 +16,11 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.util.List;
 import java.util.Optional;
@@ -88,15 +89,17 @@
   }
 
   public static String createMissingDependencyMessage(
-      Provider<InternalChangeQuery> queryProvider, String commit, String otherCommit)
-      throws OrmException {
+      @Nullable CurrentUser caller,
+      Provider<InternalChangeQuery> queryProvider,
+      String commit,
+      String otherCommit) {
     List<ChangeData> changes = queryProvider.get().enforceVisibility(true).byCommit(otherCommit);
 
     if (changes.isEmpty()) {
       return String.format(
           "Commit %s depends on commit %s which cannot be merged."
-              + " Is the change of this commit not visible or was it deleted?",
-          commit, otherCommit);
+              + " Is the change of this commit not visible to '%s' or was it deleted?",
+          commit, otherCommit, caller != null ? caller.getLoggableName() : "<user-not-available>");
     } else if (changes.size() == 1) {
       ChangeData cd = changes.get(0);
       if (cd.currentPatchSet().getRevision().get().equals(otherCommit)) {
@@ -105,8 +108,7 @@
             commit, otherCommit, cd.getId().get());
       }
       Optional<PatchSet> patchSet =
-          cd.patchSets()
-              .stream()
+          cd.patchSets().stream()
               .filter(ps -> ps.getRevision().get().equals(otherCommit))
               .findAny();
       if (patchSet.isPresent()) {
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index eabbe00..a1f56eb 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -14,16 +14,14 @@
 
 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.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;
@@ -42,8 +40,7 @@
         Project.NameKey project,
         Change.Id changeId,
         Account.Id submitter,
-        NotifyHandling notifyHandling,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify);
+        NotifyResolver.Result notify);
   }
 
   private final ExecutorService sendEmailsExecutor;
@@ -54,8 +51,7 @@
   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 final NotifyResolver.Result notify;
 
   @Inject
   EmailMerge(
@@ -66,8 +62,7 @@
       @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.requestContext = requestContext;
@@ -75,8 +70,7 @@
     this.project = project;
     this.changeId = changeId;
     this.submitter = submitter;
-    this.notifyHandling = notifyHandling;
-    this.accountsToNotify = accountsToNotify;
+    this.notify = notify;
   }
 
   void sendAsync() {
@@ -92,8 +86,7 @@
       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);
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 832c3879..831a8b8 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -39,7 +40,6 @@
 import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -109,7 +109,7 @@
   @Override
   public ChangeSet completeWithoutTopic(
       MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
-      throws OrmException, IOException, PermissionBackendException {
+      throws IOException, PermissionBackendException {
     Collection<ChangeData> visibleChanges = new ArrayList<>();
     Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -161,7 +161,7 @@
   }
 
   private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
-      Iterable<ChangeData> changes) throws OrmException {
+      Iterable<ChangeData> changes) {
     ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
         ImmutableListMultimap.builder();
     for (ChangeData cd : changes) {
@@ -202,7 +202,7 @@
     }
   }
 
-  private SubmitType submitType(ChangeData cd) throws OrmException {
+  private SubmitType submitType(ChangeData cd) {
     SubmitTypeRecord str = cd.submitTypeRecord();
     if (!str.isOk()) {
       logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
@@ -212,7 +212,7 @@
 
   private ChangeSet byCommitsOnBranchNotMerged(
       OpenRepo or, Branch.NameKey branch, Set<String> visibleHashes, Set<String> nonVisibleHashes)
-      throws OrmException, IOException {
+      throws IOException {
     List<ChangeData> potentiallyVisibleChanges =
         byCommitsOnBranchNotMerged(or, branch, visibleHashes);
     List<ChangeData> invisibleChanges =
@@ -229,7 +229,7 @@
   }
 
   private ImmutableList<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, Branch.NameKey branch, Set<String> hashes) throws OrmException, IOException {
+      OpenRepo or, Branch.NameKey branch, Set<String> hashes) throws IOException {
     if (hashes.isEmpty()) {
       return ImmutableList.of();
     }
@@ -281,8 +281,8 @@
     }
   }
 
-  private void logErrorAndThrow(String msg) throws OrmException {
+  private void logErrorAndThrow(String msg) {
     logger.atSevere().log(msg);
-    throw new OrmException(msg);
+    throw new StorageException(msg);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 5b25444..5a38adb 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,7 +36,8 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -46,16 +48,16 @@
 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.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.MergeTip;
 import com.google.gerrit.server.git.validators.MergeValidationException;
@@ -77,7 +79,6 @@
 import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -123,7 +124,7 @@
     private final ListMultimap<Change.Id, String> problems;
     private final boolean allowClosed;
 
-    private CommitStatus(ChangeSet cs, boolean allowClosed) throws OrmException {
+    private CommitStatus(ChangeSet cs, boolean allowClosed) {
       checkArgument(
           !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
       changes = cs.changesById();
@@ -228,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,7 +240,7 @@
   private MergeOpRepoManager orm;
   private CommitStatus commitStatus;
   private SubmitInput submitInput;
-  private ListMultimap<RecipientType, Account.Id> accountsToNotify;
+  private NotifyResolver.Result notify;
   private Set<Project.NameKey> allProjects;
   private boolean dryrun;
   private TopicMetrics topicMetrics;
@@ -255,7 +256,7 @@
       SubmitStrategyFactory submitStrategyFactory,
       SubmoduleOp.Factory subOpFactory,
       Provider<MergeOpRepoManager> ormProvider,
-      NotifyUtil notifyUtil,
+      NotifyResolver notifyResolver,
       TopicMetrics topicMetrics,
       RetryHelper retryHelper,
       ChangeData.Factory changeDataFactory) {
@@ -268,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;
@@ -282,7 +283,7 @@
   }
 
   public static void checkSubmitRule(ChangeData cd, boolean allowClosed)
-      throws ResourceConflictException, OrmException {
+      throws ResourceConflictException {
     PatchSet patchSet = cd.currentPatchSet();
     if (patchSet == null) {
       throw new ResourceConflictException("missing current patch set for change " + cd.getId());
@@ -331,23 +332,20 @@
     return cd.submitRecords(submitRuleOptions(allowClosed));
   }
 
-  private static String describeNotReady(ChangeData cd, SubmitRecord record) throws OrmException {
+  private static String describeNotReady(ChangeData cd, SubmitRecord record) {
     List<String> blockingConditions = new ArrayList<>();
     if (record.labels != null) {
       blockingConditions.add(describeLabels(cd, record.labels));
     }
     if (record.requirements != null) {
-      record
-          .requirements
-          .stream()
+      record.requirements.stream()
           .map(SubmitRequirement::fallbackText)
           .forEach(blockingConditions::add);
     }
     return Joiner.on("; ").join(blockingConditions);
   }
 
-  private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels)
-      throws OrmException {
+  private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels) {
     List<String> labelResults = new ArrayList<>();
     for (SubmitRecord.Label lbl : labels) {
       switch (lbl.status) {
@@ -383,12 +381,10 @@
         !cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
       try {
-        Change.Status status = cd.change().getStatus();
-        if (status != Change.Status.NEW) {
-          if (!(status == Change.Status.MERGED && allowMerged)) {
+        if (!cd.change().isNew()) {
+          if (!(cd.change().isMerged() && allowMerged)) {
             commitStatus.problem(
-                cd.getId(),
-                "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+                cd.getId(), "Change " + cd.getId() + " is " + ChangeUtil.status(cd.change()));
           }
         } else if (cd.change().isWorkInProgress()) {
           commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
@@ -397,7 +393,7 @@
         }
       } catch (ResourceConflictException e) {
         commitStatus.problem(cd.getId(), e.getMessage());
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         String msg = "Error checking submit rules for change";
         logger.atWarning().withCause(e).log("%s %s", msg, cd.getId());
         commitStatus.problem(cd.getId(), msg);
@@ -429,7 +425,6 @@
    * @param caller the identity of the caller
    * @param checkSubmitRules whether the prolog submit rules should be evaluated
    * @param submitInput parameters regarding the merge
-   * @throws OrmException an error occurred reading or writing the database.
    * @throws RestApiException if an error occurred.
    * @throws PermissionBackendException if permissions can't be checked
    * @throws IOException an error occurred reading from NoteDb.
@@ -440,10 +435,12 @@
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+      throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     this.submitInput = submitInput;
-    this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
+    this.notify =
+        notifyResolver.resolve(
+            firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
     this.dryrun = dryrun;
     this.caller = caller;
     this.ts = TimeUtil.nowTs();
@@ -521,7 +518,7 @@
         }
       } catch (IOException e) {
         // Anything before the merge attempt is an error
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
   }
@@ -531,7 +528,7 @@
       orm.close();
     }
     orm = ormProvider.get();
-    orm.setContext(ts, caller);
+    orm.setContext(ts, caller, notify);
   }
 
   private ChangeSet reloadChanges(ChangeSet changeSet) {
@@ -581,7 +578,7 @@
     ListMultimap<Branch.NameKey, ChangeData> cbb;
     try {
       cbb = cs.changesByBranch();
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IntegrationException("Error reading changes to submit", e);
     }
     Set<Branch.NameKey> branches = cbb.keySet();
@@ -674,7 +671,6 @@
                 commitStatus,
                 submissionId,
                 submitInput,
-                accountsToNotify,
                 submoduleOp,
                 dryrun);
         strategies.add(strategy);
@@ -744,7 +740,7 @@
         notes = cd.notes();
         chg = cd.change();
         st = getSubmitType(cd);
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         commitStatus.logProblem(changeId, e);
         continue;
       }
@@ -776,7 +772,7 @@
       Branch.NameKey destBranch = chg.getDest();
       try {
         ps = cd.currentPatchSet();
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         commitStatus.logProblem(changeId, e);
         continue;
       }
@@ -872,7 +868,7 @@
         revisions.put(e.getValue().getObjectId(), PatchSet.Id.fromRef(e.getKey()));
       }
       return revisions;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Failed to validate changes", e);
     }
   }
@@ -905,7 +901,7 @@
                 @Override
                 public boolean updateChange(ChangeContext ctx) {
                   Change change = ctx.getChange();
-                  if (!change.getStatus().isOpen()) {
+                  if (!change.isNew()) {
                     return false;
                   }
 
@@ -931,7 +927,7 @@
           }
         }
       }
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       logger.atWarning().withCause(e).log(
           "Cannot abandon changes for deleted project %s", destProject);
     }
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index cbaad6a..764aca8 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -15,12 +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.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;
@@ -110,6 +112,7 @@
             batchUpdateFactory
                 .create(getProjectName(), caller, ts)
                 .setRepository(repo, rw, ins)
+                .setNotify(notify)
                 .setOnSubmitValidators(onSubmitValidatorsFactory.create());
       }
       return update;
@@ -158,6 +161,7 @@
 
   private Timestamp ts;
   private IdentifiedUser caller;
+  private NotifyResolver.Result notify;
 
   @Inject
   MergeOpRepoManager(
@@ -173,9 +177,10 @@
     openRepos = new HashMap<>();
   }
 
-  public void setContext(Timestamp ts, IdentifiedUser caller) {
-    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 {
@@ -200,7 +205,7 @@
       throws NoSuchProjectException, IOException {
     List<BatchUpdate> updates = new ArrayList<>(projects.size());
     for (Project.NameKey project : projects) {
-      updates.add(getRepo(project).getUpdate().setRefLogMessage("merged"));
+      updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
     }
     return updates;
   }
diff --git a/java/com/google/gerrit/server/submit/MergeSorter.java b/java/com/google/gerrit/server/submit/MergeSorter.java
index 6dbec32..f2f6537 100644
--- a/java/com/google/gerrit/server/submit/MergeSorter.java
+++ b/java/com/google/gerrit/server/submit/MergeSorter.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.Collection;
@@ -29,6 +30,7 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 
 public class MergeSorter {
+  @Nullable private final CurrentUser caller;
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
@@ -36,11 +38,13 @@
   private final Set<CodeReviewCommit> incoming;
 
   public MergeSorter(
+      @Nullable CurrentUser caller,
       CodeReviewRevWalk rw,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
       Provider<InternalChangeQuery> queryProvider,
       Set<CodeReviewCommit> incoming) {
+    this.caller = caller;
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
@@ -49,7 +53,7 @@
   }
 
   public Collection<CodeReviewCommit> sort(Collection<CodeReviewCommit> toMerge)
-      throws IOException, OrmException {
+      throws IOException {
     final Set<CodeReviewCommit> heads = new HashSet<>();
     final Set<CodeReviewCommit> sort = new HashSet<>(toMerge);
     while (!sort.isEmpty()) {
@@ -70,7 +74,8 @@
           //
           n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
           n.setStatusMessage(
-              CommitMergeStatus.createMissingDependencyMessage(queryProvider, n.name(), c.name()));
+              CommitMergeStatus.createMissingDependencyMessage(
+                  caller, queryProvider, n.name(), c.name()));
           break;
         }
         contents.add(c);
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index 87ce6d4..d182f24 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -93,7 +92,7 @@
   }
 
   public ChangeSet completeChangeSet(Change change, CurrentUser user)
-      throws IOException, OrmException, PermissionBackendException {
+      throws IOException, PermissionBackendException {
     try {
       if (orm == null) {
         orm = repoManagerProvider.get();
@@ -146,7 +145,7 @@
    */
   private ChangeSet topicClosure(
       ChangeSet changeSet, CurrentUser user, Set<String> topicsSeen, Set<String> visibleTopicsSeen)
-      throws OrmException, PermissionBackendException, IOException {
+      throws PermissionBackendException, IOException {
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -181,7 +180,7 @@
   }
 
   private ChangeSet completeChangeSetIncludingTopics(ChangeSet changeSet, CurrentUser user)
-      throws IOException, OrmException, PermissionBackendException {
+      throws IOException, PermissionBackendException {
     Set<String> topicsSeen = new HashSet<>();
     Set<String> visibleTopicsSeen = new HashSet<>();
     int oldSeen;
@@ -201,7 +200,7 @@
     return changeSet;
   }
 
-  private List<ChangeData> byTopicOpen(String topic) throws OrmException {
+  private List<ChangeData> byTopicOpen(String topic) {
     return queryProvider.get().byTopicOpen(topic);
   }
 
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
index a487c95..99239e3 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSetComputation.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 
 /**
@@ -46,5 +45,5 @@
    * @return the completed set of changes that should be submitted together
    */
   ChangeSet completeWithoutTopic(MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
-      throws OrmException, IOException, PermissionBackendException;
+      throws IOException, PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index 32f3558..829ee9c 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -37,6 +37,7 @@
 public class RebaseSorter {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final CurrentUser caller;
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final RevCommit initialTip;
@@ -45,12 +46,14 @@
   private final Set<CodeReviewCommit> incoming;
 
   public RebaseSorter(
+      CurrentUser caller,
       CodeReviewRevWalk rw,
       RevCommit initialTip,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
       Provider<InternalChangeQuery> queryProvider,
       Set<CodeReviewCommit> incoming) {
+    this.caller = caller;
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.initialTip = initialTip;
@@ -59,8 +62,7 @@
     this.incoming = incoming;
   }
 
-  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
-      throws IOException, OrmException {
+  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) throws IOException {
     final List<CodeReviewCommit> sorted = new ArrayList<>();
     final Set<CodeReviewCommit> sort = new HashSet<>(toSort);
     while (!sort.isEmpty()) {
@@ -85,7 +87,7 @@
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
             n.setStatusMessage(
                 CommitMergeStatus.createMissingDependencyMessage(
-                    queryProvider, n.name(), c.name()));
+                    caller, queryProvider, n.name(), c.name()));
           }
           // Stop RevWalk because c is either a merged commit or a missing
           // dependency. Not need to walk further.
@@ -122,15 +124,14 @@
       // check if the commit associated change is merged in the same branch
       List<ChangeData> changes = queryProvider.get().byCommit(commit);
       for (ChangeData change : changes) {
-        if (change.change().getStatus() == Status.MERGED
-            && change.change().getDest().equals(dest)) {
+        if (change.change().isMerged() && change.change().getDest().equals(dest)) {
           logger.atFine().log(
               "Dependency %s associated with merged change %s.", commit.getName(), change.getId());
           return true;
         }
       }
       return false;
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IOException(e);
     }
   }
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index ad53c53..342ae69 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -59,7 +59,7 @@
     List<CodeReviewCommit> sorted;
     try {
       sorted = args.rebaseSorter.sort(toMerge);
-    } catch (IOException | OrmException e) {
+    } catch (IOException | StorageException e) {
       throw new IntegrationException("Commit sorting failed", e);
     }
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
@@ -119,7 +119,7 @@
     @Override
     public void updateRepoImpl(RepoContext ctx)
         throws IntegrationException, InvalidChangeOperationException, RestApiException, IOException,
-            OrmException, PermissionBackendException {
+            PermissionBackendException {
       if (args.mergeUtil.canFastForward(
           args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
         if (!rebaseAlways) {
@@ -185,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 {
@@ -213,7 +214,7 @@
 
     @Override
     public PatchSet updateChangeImpl(ChangeContext ctx)
-        throws NoSuchChangeException, ResourceConflictException, OrmException, IOException {
+        throws NoSuchChangeException, ResourceConflictException, IOException {
       if (newCommit == null) {
         checkState(!rebaseAlways, "RebaseAlways must never fast forward");
         // otherwise, took the fast-forward option, nothing to do.
@@ -245,7 +246,7 @@
     }
 
     @Override
-    public void postUpdateImpl(Context ctx) throws OrmException {
+    public void postUpdateImpl(Context ctx) {
       if (rebaseOp != null) {
         rebaseOp.postUpdate(ctx);
       }
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index e273652..391d956 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -19,8 +19,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeUtil;
@@ -106,6 +108,7 @@
   }
 
   public boolean run(
+      @Nullable CurrentUser caller,
       SubmitType submitType,
       Repository repo,
       CodeReviewRevWalk rw,
@@ -124,7 +127,12 @@
             rw,
             mergeUtilFactory.create(getProject(destBranch)),
             new MergeSorter(
-                rw, alreadyAccepted, canMerge, queryProvider, ImmutableSet.of(toMergeCommit)));
+                caller,
+                rw,
+                alreadyAccepted,
+                canMerge,
+                queryProvider,
+                ImmutableSet.of(toMergeCommit)));
 
     switch (submitType) {
       case CHERRY_PICK:
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index 16c2b27..dc221f8 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -16,13 +16,10 @@
 
 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.server.ApprovalsUtil;
@@ -33,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;
@@ -94,7 +92,6 @@
           Set<CodeReviewCommit> incoming,
           RequestId submissionId,
           SubmitInput submitInput,
-          ListMultimap<RecipientType, Account.Id> accountsToNotify,
           SubmoduleOp submoduleOp,
           boolean dryrun);
     }
@@ -115,6 +112,7 @@
     final TagCache tagCache;
     final Provider<InternalChangeQuery> queryProvider;
     final ProjectConfig.Factory projectConfigFactory;
+    final SetPrivateOp.Factory setPrivateOpFactory;
 
     final Branch.NameKey destBranch;
     final CodeReviewRevWalk rw;
@@ -126,7 +124,6 @@
     final RequestId submissionId;
     final SubmitType submitType;
     final SubmitInput submitInput;
-    final ListMultimap<RecipientType, Account.Id> accountsToNotify;
     final SubmoduleOp submoduleOp;
 
     final ProjectState project;
@@ -154,6 +151,7 @@
         TagCache tagCache,
         Provider<InternalChangeQuery> queryProvider,
         ProjectConfig.Factory projectConfigFactory,
+        SetPrivateOp.Factory setPrivateOpFactory,
         @Assisted Branch.NameKey destBranch,
         @Assisted CommitStatus commitStatus,
         @Assisted CodeReviewRevWalk rw,
@@ -165,7 +163,6 @@
         @Assisted RequestId submissionId,
         @Assisted SubmitType submitType,
         @Assisted SubmitInput submitInput,
-        @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
         @Assisted SubmoduleOp submoduleOp,
         @Assisted boolean dryrun) {
       this.accountCache = accountCache;
@@ -182,6 +179,7 @@
       this.rebaseFactory = rebaseFactory;
       this.tagCache = tagCache;
       this.queryProvider = queryProvider;
+      this.setPrivateOpFactory = setPrivateOpFactory;
 
       this.serverIdent = serverIdent;
       this.destBranch = destBranch;
@@ -194,7 +192,6 @@
       this.submissionId = submissionId;
       this.submitType = submitType;
       this.submitInput = submitInput;
-      this.accountsToNotify = accountsToNotify;
       this.submoduleOp = submoduleOp;
       this.dryrun = dryrun;
 
@@ -203,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;
     }
@@ -245,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 0a92d61..30326f7 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -14,12 +14,9 @@
 
 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.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -57,7 +54,6 @@
       CommitStatus commitStatus,
       RequestId submissionId,
       SubmitInput submitInput,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify,
       SubmoduleOp submoduleOp,
       boolean dryrun)
       throws IntegrationException {
@@ -74,7 +70,6 @@
             incoming,
             submissionId,
             submitInput,
-            accountsToNotify,
             submoduleOp,
             dryrun);
     switch (submitType) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 8a6f914..73bcc09 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Function;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -215,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) {
@@ -283,7 +283,7 @@
             : alreadyMergedCommit;
     try {
       setMerged(ctx, message(ctx, commit, s));
-    } catch (OrmException err) {
+    } catch (StorageException err) {
       String msg = "Error updating change status for " + id;
       logger.atSevere().withCause(err).log(msg);
       args.commitStatus.logProblem(id, msg);
@@ -294,8 +294,7 @@
     return true;
   }
 
-  private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
-      throws IOException, OrmException {
+  private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) throws IOException {
     PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
     logger.atFine().log("Fixing up already-merged patch set %s", psId);
     PatchSet prevPs = args.psUtil.current(ctx.getNotes());
@@ -317,8 +316,7 @@
         ctx.getRevWalk(), ctx.getUpdate(psId), psId, alreadyMergedCommit, groups, null, null);
   }
 
-  private void setApproval(ChangeContext ctx, IdentifiedUser user)
-      throws OrmException, IOException {
+  private void setApproval(ChangeContext ctx, IdentifiedUser user) throws IOException {
     Change.Id id = ctx.getChange().getId();
     List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
     PatchSet.Id oldPsId = toMerge.getPatchsetId();
@@ -340,7 +338,7 @@
   }
 
   private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
-      throws OrmException, IOException {
+      throws IOException {
     PatchSet.Id psId = update.getPatchSetId();
     Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
     for (PatchSetApproval psa :
@@ -404,8 +402,7 @@
     return "";
   }
 
-  private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s)
-      throws OrmException {
+  private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s) {
     requireNonNull(s, "CommitMergeStatus may not be null");
     String txt = s.getDescription();
     if (s == CommitMergeStatus.CLEAN_MERGE) {
@@ -501,12 +498,7 @@
     // have failed fast in one of the other steps.
     try {
       args.mergedSenderFactory
-          .create(
-              ctx.getProject(),
-              getId(),
-              submitter.getAccountId(),
-              args.submitInput.notify,
-              args.accountsToNotify)
+          .create(ctx.getProject(), getId(), submitter.getAccountId(), ctx.getNotify(getId()))
           .sendAsync();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index 3fefed3..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;
@@ -444,9 +444,7 @@
     int count = 0;
 
     List<SubmoduleSubscription> subscriptions =
-        targets
-            .get(subscriber)
-            .stream()
+        targets.get(subscriber).stream()
             .sorted(comparing(SubmoduleSubscription::getPath))
             .collect(toList());
     for (SubmoduleSubscription s : subscriptions) {
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 06397d7..1fc75c4 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -29,7 +29,10 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multiset;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -40,6 +43,7 @@
 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;
@@ -48,11 +52,11 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.TooManyUpdatesException;
 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.assistedinject.Assisted;
@@ -123,9 +127,7 @@
     checkDifferentProject(updates);
 
     try {
-      @SuppressWarnings("deprecation")
-      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
-          new ArrayList<>();
+      List<ListenableFuture<?>> indexFutures = new ArrayList<>();
       List<ChangesHandle> handles = new ArrayList<>(updates.size());
       try {
         for (BatchUpdate u : updates) {
@@ -147,13 +149,12 @@
         }
       }
 
-      ChangeIndexer.allAsList(indexFutures).get();
+      ((ListenableFuture<?>) Futures.allAsList(indexFutures)).get();
 
       // Fire ref update events only after all mutations are finished, since callers may assume a
       // patch set ref being created means the change was created, or a branch advancing meaning
       // some changes were closed.
-      updates
-          .stream()
+      updates.stream()
           .filter(u -> u.batchRefUpdate != null)
           .forEach(
               u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
@@ -178,16 +179,9 @@
   }
 
   private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
-    Throwables.throwIfUnchecked(e);
-
-    // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
-    // ResourceConflictException to indicate an atomic update failure.
-    Throwables.throwIfInstanceOf(e, UpdateException.class);
-    Throwables.throwIfInstanceOf(e, RestApiException.class);
-
-    // Convert other common non-REST exception types with user-visible messages to corresponding
-    // REST exception types
-    if (e instanceof InvalidChangeOperationException) {
+    // Convert common non-REST exception types with user-visible messages to corresponding REST
+    // exception types.
+    if (e instanceof InvalidChangeOperationException || e instanceof TooManyUpdatesException) {
       throw new ResourceConflictException(e.getMessage(), e);
     } else if (e instanceof NoSuchChangeException
         || e instanceof NoSuchRefException
@@ -195,6 +189,13 @@
       throw new ResourceNotFoundException(e.getMessage(), e);
     }
 
+    Throwables.throwIfUnchecked(e);
+
+    // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
+    // ResourceConflictException to indicate an atomic update failure.
+    Throwables.throwIfInstanceOf(e, UpdateException.class);
+    Throwables.throwIfInstanceOf(e, RestApiException.class);
+
     // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
     throw new UpdateException(e);
   }
@@ -229,6 +230,12 @@
     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 {
@@ -302,12 +309,14 @@
       MultimapBuilder.linkedHashKeys().arrayListValues().build();
   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<>();
 
   private RepoView repoView;
   private BatchRefUpdate batchRefUpdate;
   private OnSubmitValidators onSubmitValidators;
   private PushCertificate pushCert;
   private String refLogMessage;
+  private NotifyResolver.Result notify = NotifyResolver.Result.all();
 
   @Inject
   BatchUpdate(
@@ -365,6 +374,32 @@
   }
 
   /**
+   * 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;
+  }
+
+  /**
    * Add a validation step for intended ref operations, which will be performed at the end of {@link
    * RepoOnlyOp#updateRepo(RepoContext)} step.
    */
@@ -465,18 +500,16 @@
       checkArgument(old == null, "result for change %s already set: %s", id, old);
     }
 
-    void execute() throws OrmException, IOException {
+    void execute() throws IOException {
       BatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
     }
 
-    @SuppressWarnings("deprecation")
-    List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> startIndexFutures() {
+    List<ListenableFuture<?>> 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());
+      List<ListenableFuture<?>> indexFutures = new ArrayList<>(results.size());
       for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
         Change.Id id = e.getKey();
         switch (e.getValue()) {
@@ -548,7 +581,7 @@
     return handle;
   }
 
-  private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException {
+  private ChangeContextImpl newChangeContext(Change.Id id) {
     logDebug("Opening change %s for update", id);
     Change c = newChanges.get(id);
     boolean isNew = c != null;
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index c24c650..8704cf0 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -17,10 +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.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;
@@ -86,6 +88,18 @@
   CurrentUser getUser();
 
   /**
+   * Get the notification settings configured by the caller.
+   *
+   * <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.
+   */
+  NotifyResolver.Result getNotify(Change.Id changeId);
+
+  /**
    * Get the identified user performing the update.
    *
    * <p>Convenience method for {@code getUser().asIdentifiedUser()}.
diff --git a/java/com/google/gerrit/server/update/RepoView.java b/java/com/google/gerrit/server/update/RepoView.java
index 23853ee..73dd12f 100644
--- a/java/com/google/gerrit/server/update/RepoView.java
+++ b/java/com/google/gerrit/server/update/RepoView.java
@@ -136,9 +136,7 @@
    */
   public Map<String, ObjectId> getRefs(String prefix) throws IOException {
     Map<String, ObjectId> result =
-        repo.getRefDatabase()
-            .getRefsByPrefix(prefix)
-            .stream()
+        repo.getRefDatabase().getRefsByPrefix(prefix).stream()
             .collect(toMap(r -> r.getName().substring(prefix.length()), Ref::getObjectId));
 
     // First, overwrite any cached reads from the underlying RepoRefCache. If any of these differ,
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index d2bccf1..ae8ba53 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -68,7 +68,8 @@
     ACCOUNT_UPDATE,
     CHANGE_UPDATE,
     GROUP_UPDATE,
-    INDEX_QUERY
+    INDEX_QUERY,
+    PLUGIN_UPDATE
   }
 
   /**
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/MostSpecificComparator.java b/java/com/google/gerrit/server/util/MostSpecificComparator.java
index f243726..b22617c 100644
--- a/java/com/google/gerrit/server/util/MostSpecificComparator.java
+++ b/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.server.project.RefPattern;
 import java.util.Comparator;
 import org.apache.commons.lang.StringUtils;
@@ -40,7 +40,7 @@
  * are infinite, but refs/heads/[a-zA-Z]* has more transitions, which after all turns it more
  * specific.
  */
-public final class MostSpecificComparator implements Comparator<RefConfigSection> {
+public final class MostSpecificComparator implements Comparator<AccessSection> {
   private final String refName;
 
   public MostSpecificComparator(String refName) {
@@ -48,7 +48,7 @@
   }
 
   @Override
-  public int compare(RefConfigSection a, RefConfigSection b) {
+  public int compare(AccessSection a, AccessSection b) {
     return compare(a.getName(), b.getName());
   }
 
diff --git a/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index 06e93f8..afd699c 100644
--- a/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.AbstractModule;
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index ac73482..42ef7d5 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -5,6 +5,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/lifecycle",
@@ -24,7 +25,6 @@
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:jsch",
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index c8586f2..67df1c6 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -55,13 +54,13 @@
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     addChange(id, changes, null);
   }
 
   public void addChange(
       String id, Map<Change.Id, ChangeResource> changes, ProjectState projectState)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     addChange(id, changes, projectState, true);
   }
 
@@ -70,7 +69,7 @@
       Map<Change.Id, ChangeResource> changes,
       ProjectState projectState,
       boolean useIndex)
-      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
+      throws UnloggedFailure, PermissionBackendException, IOException {
     List<ChangeNotes> matched = useIndex ? changeFinder.find(id) : changeFromNotesFactory(id);
     List<ChangeNotes> toAdd = new ArrayList<>(changes.size());
     boolean canMaintainServer;
@@ -116,7 +115,7 @@
     changes.put(cId, changeResource);
   }
 
-  private List<ChangeNotes> changeFromNotesFactory(String id) throws OrmException, UnloggedFailure {
+  private List<ChangeNotes> changeFromNotesFactory(String id) throws UnloggedFailure {
     return changeNotesFactory.create(parseId(id));
   }
 
diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java
index 4c9ca91..68962db 100644
--- a/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.args4j.SubcommandHandler;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -44,6 +45,7 @@
   private final PermissionBackend permissionBackend;
   private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
+  private final DynamicSet<SshExecuteCommandInterceptor> commandInterceptors;
 
   @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
   private String commandName;
@@ -52,10 +54,14 @@
   private List<String> args = new ArrayList<>();
 
   @Inject
-  DispatchCommand(PermissionBackend permissionBackend, @Assisted Map<String, CommandProvider> all) {
+  DispatchCommand(
+      PermissionBackend permissionBackend,
+      DynamicSet<SshExecuteCommandInterceptor> commandInterceptors,
+      @Assisted Map<String, CommandProvider> all) {
     this.permissionBackend = permissionBackend;
     commands = all;
     atomicCmd = Atomics.newReference();
+    this.commandInterceptors = commandInterceptors;
   }
 
   Map<String, CommandProvider> getMap() {
@@ -84,19 +90,29 @@
 
       final Command cmd = p.getProvider().get();
       checkRequiresCapability(cmd);
+      String actualCommandName = commandName;
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
-        if (getName().isEmpty()) {
-          bc.setName(commandName);
-        } else {
-          bc.setName(getName() + " " + commandName);
+        if (!getName().isEmpty()) {
+          actualCommandName = getName() + " " + commandName;
         }
+        bc.setName(actualCommandName);
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
         throw die(commandName + " does not take arguments");
       }
 
+      for (SshExecuteCommandInterceptor commandInterceptor : commandInterceptors) {
+        if (!commandInterceptor.accept(actualCommandName, args)) {
+          throw new UnloggedFailure(
+              126,
+              String.format(
+                  "blocked by %s, contact gerrit administrators for more details",
+                  commandInterceptor.name()));
+        }
+      }
+
       provideStateTo(cmd);
       atomicCmd.set(cmd);
       cmd.start(env);
diff --git a/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java b/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java
new file mode 100644
index 0000000..ee60670
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.List;
+
+@ExtensionPoint
+public interface SshExecuteCommandInterceptor {
+
+  /**
+   * Check the command and return false if this command must not be run.
+   *
+   * @param command the command
+   * @param arguments the list of arguments
+   * @return whether or not this command with these arguments can be executed
+   */
+  boolean accept(String command, List<String> arguments);
+
+  default String name() {
+    return this.getClass().getSimpleName();
+  }
+}
diff --git a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
index d89f9e0..da0ec1d 100644
--- a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.ssh.SshKeyCreator;
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index df3242c..5fb75c8 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.audit.SshAuditEvent;
 import com.google.gerrit.server.config.ConfigKey;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
@@ -29,6 +28,7 @@
 import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -57,7 +57,7 @@
   private final Provider<SshSession> session;
   private final Provider<Context> context;
   private volatile AsyncAppender async;
-  private final AuditService auditService;
+  private final GroupAuditService auditService;
   private final SystemLog systemLog;
 
   private final Object lock = new Object();
@@ -68,7 +68,7 @@
       final Provider<Context> context,
       SystemLog systemLog,
       @GerritServerConfig Config config,
-      AuditService auditService) {
+      GroupAuditService auditService) {
     this.session = session;
     this.context = context;
     this.auditService = auditService;
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index 1de14b6..e4aa14c 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -20,10 +20,8 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritConfigListener;
@@ -102,8 +100,8 @@
         .annotatedWith(UniqueAnnotations.create())
         .to(SshPluginStarterCallback.class);
 
-    DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
     DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
+    DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
 
     listener().toInstance(registerInParentInjectors());
     listener().to(SshLog.class);
diff --git a/java/com/google/gerrit/sshd/commands/ApproveOption.java b/java/com/google/gerrit/sshd/commands/ApproveOption.java
deleted file mode 100644
index cda340d..0000000
--- a/java/com/google/gerrit/sshd/commands/ApproveOption.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import static com.google.gerrit.util.cli.Localizable.localizable;
-
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.CmdLineParser;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.OptionDef;
-import org.kohsuke.args4j.spi.FieldSetter;
-import org.kohsuke.args4j.spi.OneArgumentOptionHandler;
-import org.kohsuke.args4j.spi.OptionHandler;
-import org.kohsuke.args4j.spi.Setter;
-
-final class ApproveOption implements Option, Setter<Short> {
-  private final String name;
-  private final String usage;
-  private final LabelType type;
-
-  private Short value;
-
-  ApproveOption(String name, String usage, LabelType type) {
-    this.name = name;
-    this.usage = usage;
-    this.type = type;
-  }
-
-  @Override
-  public String[] aliases() {
-    return new String[0];
-  }
-
-  @Override
-  public String[] depends() {
-    return new String[] {};
-  }
-
-  @Override
-  public boolean hidden() {
-    return false;
-  }
-
-  @Override
-  public Class<? extends OptionHandler<Short>> handler() {
-    return Handler.class;
-  }
-
-  @Override
-  public String metaVar() {
-    return "N";
-  }
-
-  @Override
-  public String name() {
-    return name;
-  }
-
-  @Override
-  public boolean required() {
-    return false;
-  }
-
-  @Override
-  public String usage() {
-    return usage;
-  }
-
-  public Short value() {
-    return value;
-  }
-
-  @Override
-  public Class<? extends Annotation> annotationType() {
-    return null;
-  }
-
-  @Override
-  public FieldSetter asFieldSetter() {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public AnnotatedElement asAnnotatedElement() {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void addValue(Short val) {
-    this.value = val;
-  }
-
-  @Override
-  public Class<Short> getType() {
-    return Short.class;
-  }
-
-  @Override
-  public boolean isMultiValued() {
-    return false;
-  }
-
-  @Override
-  public String[] forbids() {
-    return null;
-  }
-
-  @Override
-  public boolean help() {
-    return false;
-  }
-
-  String getLabelName() {
-    return type.getName();
-  }
-
-  public static class Handler extends OneArgumentOptionHandler<Short> {
-    private final ApproveOption cmdOption;
-
-    // CS IGNORE RedundantModifier FOR NEXT 1 LINES. REASON: needed by org.kohsuke.args4j.Option
-    public Handler(CmdLineParser parser, OptionDef option, Setter<Short> setter) {
-      super(parser, option, setter);
-      this.cmdOption = (ApproveOption) setter;
-    }
-
-    @Override
-    protected Short parse(String token) throws NumberFormatException, CmdLineException {
-      String argument = token;
-      if (argument.startsWith("+")) {
-        argument = argument.substring(1);
-      }
-
-      final short value = Short.parseShort(argument);
-      final LabelValue min = cmdOption.type.getMin();
-      final LabelValue max = cmdOption.type.getMax();
-
-      if (value < min.getValue() || value > max.getValue()) {
-        final String name = cmdOption.name();
-        final String e =
-            "\""
-                + token
-                + "\" must be in range "
-                + min.formatValue()
-                + ".."
-                + max.formatValue()
-                + " for \""
-                + name
-                + "\"";
-        throw new CmdLineException(owner, localizable(e));
-      }
-      return value;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 8a37cce..8875f07 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -72,8 +71,7 @@
 
   @Override
   protected void run()
-      throws OrmException, IOException, ConfigInvalidException, UnloggedFailure,
-          PermissionBackendException {
+      throws IOException, ConfigInvalidException, UnloggedFailure, PermissionBackendException {
     AccountInput input = new AccountInput();
     input.username = username;
     input.email = email;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 917c138..f9a04a0 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.HashSet;
@@ -102,8 +101,7 @@
 
   @Override
   protected void run()
-      throws Failure, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws Failure, IOException, ConfigInvalidException, PermissionBackendException {
     try {
       GroupResource rsrc = createGroup();
 
@@ -120,8 +118,7 @@
   }
 
   private GroupResource createGroup()
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
@@ -136,8 +133,7 @@
   }
 
   private void addMembers(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     AddMembers.Input input =
         AddMembers.Input.fromMembers(
             initialMembers.stream().map(Object::toString).collect(toList()));
@@ -145,8 +141,7 @@
   }
 
   private void addSubgroups(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     AddSubgroups.Input input =
         AddSubgroups.Input.fromGroups(
             initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index fad74f5..5aa2ec8 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
@@ -22,7 +23,6 @@
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.LinkedHashMap;
@@ -44,7 +44,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
-    } catch (UnloggedFailure | OrmException | PermissionBackendException | IOException e) {
+    } catch (UnloggedFailure | StorageException | PermissionBackendException | IOException e) {
       writeError("warning", e.getMessage());
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d04e2d3..9f2ffa9 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -41,6 +41,6 @@
         throw die("--tree and --description options are not compatible.");
       }
     }
-    impl.display(out);
+    impl.displayToStream(out);
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 969ce50..1f991e0 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -17,7 +17,9 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -31,7 +33,6 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Map;
@@ -73,21 +74,20 @@
 
   @Override
   protected void run() throws Failure {
-    Account userAccount;
+    Account.Id userAccountId;
     try {
-      userAccount = accountResolver.find(userName);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
-      throw die(e);
-    }
-    if (userAccount == null) {
-      stdout.print("No single user could be found when searching for: " + userName + '\n');
+      userAccountId = accountResolver.resolve(userName).asUnique().getAccount().getId();
+    } catch (UnprocessableEntityException e) {
+      stdout.println(e.getMessage());
       stdout.flush();
       return;
+    } catch (StorageException | IOException | ConfigInvalidException e) {
+      throw die(e);
     }
 
     Project.NameKey projectName = projectState.getNameKey();
     try (Repository repo = repoManager.openRepository(projectName);
-        ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
+        ManualRequestContext ctx = requestContext.openAs(userAccountId)) {
       try {
         Map<String, Ref> refsMap =
             permissionBackend
diff --git a/java/com/google/gerrit/sshd/commands/PatchSetParser.java b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
index d174561..a6e802a 100644
--- a/java/com/google/gerrit/sshd/commands/PatchSetParser.java
+++ b/java/com/google/gerrit/sshd/commands/PatchSetParser.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -54,7 +53,7 @@
   }
 
   public PatchSet parsePatchSet(String token, ProjectState projectState, String branch)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure {
     // By commit?
     //
     if (token.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
@@ -123,7 +122,7 @@
   }
 
   private ChangeNotes getNotes(@Nullable ProjectState projectState, Change.Id changeId)
-      throws OrmException, UnloggedFailure {
+      throws UnloggedFailure {
     if (projectState != null) {
       return notesFactory.create(projectState.getNameKey(), changeId);
     }
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/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index fa2d894..7af07de 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.util.cli.Localizable.localizable;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -40,20 +43,26 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gerrit.util.cli.OptionUtil;
 import com.google.gson.JsonSyntaxException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.ArrayList;
+import java.lang.reflect.AnnotatedElement;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.FieldSetter;
+import org.kohsuke.args4j.spi.OneArgumentOptionHandler;
+import org.kohsuke.args4j.spi.Setter;
 
 @CommandMetaData(name = "review", description = "Apply reviews to one or more patch sets")
 public class ReviewCommand extends SshCommand {
@@ -61,10 +70,8 @@
 
   @Override
   protected final CmdLineParser newCmdLineParser(Object options) {
-    final CmdLineParser parser = super.newCmdLineParser(options);
-    for (ApproveOption c : optionList) {
-      parser.addOption(c, c);
-    }
+    CmdLineParser parser = super.newCmdLineParser(options);
+    optionMap.forEach((o, s) -> parser.addOption(s, o));
     return parser;
   }
 
@@ -82,7 +89,7 @@
       patchSets.add(ps);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IllegalArgumentException("database error", e);
     }
   }
@@ -154,7 +161,7 @@
 
   @Inject private PatchSetParser psParser;
 
-  private List<ApproveOption> optionList;
+  private Map<Option, LabelSetter> optionMap;
   private Map<String, Short> customLabels;
 
   @Override
@@ -257,11 +264,8 @@
     review.notify = notify;
     review.labels = new TreeMap<>();
     review.drafts = ReviewInput.DraftHandling.PUBLISH;
-    for (ApproveOption ao : optionList) {
-      Short v = ao.value();
-      if (v != null) {
-        review.labels.put(ao.getLabelName(), v);
-      }
+    for (LabelSetter setter : optionMap.values()) {
+      setter.getValue().ifPresent(v -> review.labels.put(setter.getLabelName(), v));
     }
     review.labels.putAll(customLabels);
 
@@ -315,7 +319,7 @@
 
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
-    optionList = new ArrayList<>();
+    optionMap = new LinkedHashMap<>();
     customLabels = new HashMap<>();
 
     ProjectState allProjectsState;
@@ -332,10 +336,111 @@
         usage.append(v.format()).append("\n");
       }
 
-      final String name = "--" + type.getName().toLowerCase();
-      optionList.add(new ApproveOption(name, usage.toString(), type));
+      optionMap.put(newApproveOption(type, usage.toString()), new LabelSetter(type));
     }
 
     super.parseCommandLine();
   }
+
+  private static String asOptionName(LabelType type) {
+    return "--" + type.getName().toLowerCase();
+  }
+
+  private static Option newApproveOption(LabelType type, String usage) {
+    return OptionUtil.newOption(
+        asOptionName(type),
+        new String[0],
+        usage,
+        "N",
+        false,
+        false,
+        false,
+        LabelHandler.class,
+        new String[0],
+        new String[0]);
+  }
+
+  private static class LabelSetter implements Setter<Short> {
+    private final LabelType type;
+    private Optional<Short> value;
+
+    LabelSetter(LabelType type) {
+      this.type = requireNonNull(type);
+      this.value = Optional.empty();
+    }
+
+    Optional<Short> getValue() {
+      return value;
+    }
+
+    LabelType getLabelType() {
+      return type;
+    }
+
+    String getLabelName() {
+      return type.getName();
+    }
+
+    @Override
+    public void addValue(Short value) {
+      this.value = Optional.of(value);
+    }
+
+    @Override
+    public Class<Short> getType() {
+      return Short.class;
+    }
+
+    @Override
+    public boolean isMultiValued() {
+      return false;
+    }
+
+    @Override
+    public FieldSetter asFieldSetter() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AnnotatedElement asAnnotatedElement() {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  public static class LabelHandler extends OneArgumentOptionHandler<Short> {
+    private final LabelType type;
+
+    public LabelHandler(
+        org.kohsuke.args4j.CmdLineParser parser, OptionDef option, Setter<Short> setter) {
+      super(parser, option, setter);
+      this.type = ((LabelSetter) setter).getLabelType();
+    }
+
+    @Override
+    protected Short parse(String token) throws NumberFormatException, CmdLineException {
+      String argument = token;
+      if (argument.startsWith("+")) {
+        argument = argument.substring(1);
+      }
+
+      short value = Short.parseShort(argument);
+      LabelValue min = type.getMin();
+      LabelValue max = type.getMax();
+
+      if (value < min.getValue() || value > max.getValue()) {
+        String e =
+            "\""
+                + token
+                + "\" must be in range "
+                + min.formatValue()
+                + ".."
+                + max.formatValue()
+                + " for \""
+                + asOptionName(type)
+                + "\"";
+        throw new CmdLineException(owner, localizable(e));
+      }
+      return value;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/sshd/commands/ScpCommand.java b/java/com/google/gerrit/sshd/commands/ScpCommand.java
index b3a9a16..5122b35 100644
--- a/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -27,7 +27,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.tools.ToolsCatalog;
-import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
@@ -104,14 +103,14 @@
           root = "";
         }
 
-        final Entry ent = toc.get(root);
+        final ToolsCatalog.Entry ent = toc.get(root);
         if (ent == null) {
           throw new IOException(root + " not found");
 
-        } else if (Entry.Type.FILE == ent.getType()) {
+        } else if (ToolsCatalog.Entry.Type.FILE == ent.getType()) {
           readFile(ent);
 
-        } else if (Entry.Type.DIR == ent.getType()) {
+        } else if (ToolsCatalog.Entry.Type.DIR == ent.getType()) {
           if (!opt_r) {
             throw new IOException(root + " not a regular file");
           }
@@ -156,7 +155,7 @@
     }
   }
 
-  private void readFile(Entry ent) throws IOException {
+  private void readFile(ToolsCatalog.Entry ent) throws IOException {
     byte[] data = ent.getBytes();
     if (data == null) {
       throw new FileNotFoundException(ent.getPath());
@@ -170,12 +169,12 @@
     readAck();
   }
 
-  private void readDir(Entry dir) throws IOException {
+  private void readDir(ToolsCatalog.Entry dir) throws IOException {
     header(dir, 0);
     readAck();
 
-    for (Entry e : dir.getChildren()) {
-      if (Entry.Type.DIR == e.getType()) {
+    for (ToolsCatalog.Entry e : dir.getChildren()) {
+      if (ToolsCatalog.Entry.Type.DIR == e.getType()) {
         readDir(e);
       } else {
         readFile(e);
@@ -187,7 +186,8 @@
     readAck();
   }
 
-  private void header(Entry dir, int len) throws IOException, UnsupportedEncodingException {
+  private void header(ToolsCatalog.Entry dir, int len)
+      throws IOException, UnsupportedEncodingException {
     final StringBuilder buf = new StringBuilder();
     switch (dir.getType()) {
       case DIR:
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 9bcb103..e4ea40d 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -18,7 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.RawInputUtil;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -52,7 +52,6 @@
 import com.google.gerrit.server.restapi.account.PutPreferred;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.BufferedReader;
@@ -213,8 +212,7 @@
   }
 
   private void setAccount()
-      throws OrmException, IOException, UnloggedFailure, ConfigInvalidException,
-          PermissionBackendException {
+      throws IOException, UnloggedFailure, ConfigInvalidException, PermissionBackendException {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user.asIdentifiedUser());
     try {
@@ -273,8 +271,7 @@
   }
 
   private void addSshKeys(List<String> sshKeys)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     for (String sshKey : sshKeys) {
       SshKeyInput in = new SshKeyInput();
       in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "plain/text");
@@ -283,8 +280,8 @@
   }
 
   private void deleteSshKeys(List<String> sshKeys)
-      throws RestApiException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
@@ -302,14 +299,14 @@
   }
 
   private void deleteSshKey(SshKeyInfo i)
-      throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
     deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
   }
 
   private void addEmail(String email)
-      throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException,
+      throws UnloggedFailure, RestApiException, IOException, ConfigInvalidException,
           PermissionBackendException {
     EmailInput in = new EmailInput();
     in.email = email;
@@ -322,8 +319,7 @@
   }
 
   private void deleteEmail(String email)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (email.equals("ALL")) {
       List<EmailInfo> emails = getEmails.apply(rsrc);
       for (EmailInfo e : emails) {
@@ -335,8 +331,7 @@
   }
 
   private void putPreferred(String email)
-      throws RestApiException, OrmException, IOException, PermissionBackendException,
-          ConfigInvalidException {
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
     for (EmailInfo e : getEmails.apply(rsrc)) {
       if (e.email.equals(email)) {
         putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
diff --git a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index 9d7f2d9..324257b 100644
--- a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -132,8 +132,7 @@
       String action, GroupResource group, List<Account.Id> accountIdList)
       throws UnsupportedEncodingException, IOException {
     String names =
-        accountIdList
-            .stream()
+        accountIdList.stream()
             .map(
                 accountId -> {
                   Optional<AccountState> accountState = accountCache.get(accountId);
@@ -152,8 +151,7 @@
       String action, GroupResource group, List<AccountGroup.UUID> groupUuidList)
       throws UnsupportedEncodingException, IOException {
     String names =
-        groupUuidList
-            .stream()
+        groupUuidList.stream()
             .map(uuid -> groupCache.get(uuid).map(InternalGroup::getName))
             .flatMap(Streams::stream)
             .collect(joining(", "));
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index 56ee371..dfdf7f2 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.projects.ParentInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -111,7 +112,7 @@
         childProjects.addAll(getChildrenForReparenting(oldParent));
       } catch (PermissionBackendException e) {
         throw new Failure(1, "permissions unavailable", e);
-      } catch (RestApiException e) {
+      } catch (StorageException | RestApiException e) {
         throw new Failure(1, "failure in request", e);
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index a4a8ea8..30caa43 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -29,7 +30,6 @@
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -75,7 +75,7 @@
       changeArgumentParser.addChange(token, changes, projectState);
     } catch (IOException | UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       throw new IllegalArgumentException("database is down", e);
     } catch (PermissionBackendException e) {
       throw new IllegalArgumentException("can't check permissions", e);
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index e4c14d8..be13a84 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -47,7 +47,6 @@
 import java.util.Collection;
 import java.util.Date;
 import java.util.Map;
-import java.util.Map.Entry;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.mina.MinaSession;
@@ -267,7 +266,7 @@
         stdout.print(String.format(" %14s", s.name()));
       }
       stdout.print('\n');
-      for (Entry<String, Map<Thread.State, Integer>> e : threadSummary.counts.entrySet()) {
+      for (Map.Entry<String, Map<Thread.State, Integer>> e : threadSummary.counts.entrySet()) {
         stdout.print(String.format("  %-22s", e.getKey()));
         for (Thread.State s : Thread.State.values()) {
           stdout.print(String.format(" %14d", nullToZero(e.getValue().get(s))));
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 0faf803..d579ef6 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -91,10 +91,7 @@
     }
 
     final ImmutableList<IoSession> list =
-        acceptor
-            .getManagedSessions()
-            .values()
-            .stream()
+        acceptor.getManagedSessions().values().stream()
             .sorted(
                 (arg0, arg1) -> {
                   if (arg0 instanceof MinaSession) {
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 71efda6..ec5076e 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -14,6 +14,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/index",
@@ -34,7 +35,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:h2",
         "//lib:junit",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index bbfd9b1..a60995b 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -21,7 +21,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.MailHeader;
@@ -150,8 +150,7 @@
   public List<Message> getMessages(String changeId, String type) {
     final String idFooter = "\n" + MailHeader.CHANGE_ID.withDelimiter() + changeId + "\n";
     final String typeFooter = "\n" + MailHeader.MESSAGE_TYPE.withDelimiter() + type + "\n";
-    return getMessages()
-        .stream()
+    return getMessages().stream()
         .filter(in -> in.body().contains(idFooter) && in.body().contains(typeFooter))
         .collect(toList());
   }
diff --git a/java/com/google/gerrit/testing/FakeGroupAuditService.java b/java/com/google/gerrit/testing/FakeGroupAuditService.java
deleted file mode 100644
index 7765e02..0000000
--- a/java/com/google/gerrit/testing/FakeGroupAuditService.java
+++ /dev/null
@@ -1,115 +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 final List<AuditEvent> auditEvents = new ArrayList<>();
-
-  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 void clearEvents() {
-    auditEvents.clear();
-  }
-
-  @Override
-  public void dispatch(AuditEvent action) {
-    synchronized (auditEvents) {
-      auditEvents.add(action);
-      auditEvents.notifyAll();
-    }
-  }
-
-  @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/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 2a030ac..c8cea6f 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -58,10 +58,10 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
-import com.google.gerrit.server.git.ChangeRefCache;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.PerThreadRequestScope;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
@@ -168,7 +168,7 @@
     factory(PluginUser.Factory.class);
     install(new PluginApiModule());
     install(new DefaultPermissionBackendModule());
-    install(new ChangeRefCache.Module());
+    install(new SearchingChangeCacheImpl.Module());
     factory(GarbageCollection.Factory.class);
     install(new AuditModule());
 
diff --git a/java/com/google/gerrit/testing/IndexVersions.java b/java/com/google/gerrit/testing/IndexVersions.java
index fde93b2..3281ffc 100644
--- a/java/com/google/gerrit/testing/IndexVersions.java
+++ b/java/com/google/gerrit/testing/IndexVersions.java
@@ -131,8 +131,7 @@
       List<Integer> schemaVersions,
       String testSuiteNamePrefix,
       Config baseConfig) {
-    return schemaVersions
-        .stream()
+    return schemaVersions.stream()
         .collect(
             toMap(
                 i -> testSuiteNamePrefix + i,
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/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index 786ae0d..6f958b1 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -4,6 +4,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/java/com/google/gerrit/truth/CacheStatsSubject.java b/java/com/google/gerrit/truth/CacheStatsSubject.java
new file mode 100644
index 0000000..f1a9393
--- /dev/null
+++ b/java/com/google/gerrit/truth/CacheStatsSubject.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.cache.CacheStats;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+
+@UsedAt(Project.PLUGINS_ALL)
+public class CacheStatsSubject extends Subject<CacheStatsSubject, CacheStats> {
+  public static CacheStatsSubject assertThat(CacheStats stats) {
+    return assertAbout(CacheStatsSubject::new).that(stats);
+  }
+
+  public static CacheStats cloneStats(CacheStats other) {
+    return new CacheStats(
+        other.hitCount(),
+        other.missCount(),
+        other.loadSuccessCount(),
+        other.loadExceptionCount(),
+        other.totalLoadTime(),
+        other.evictionCount());
+  }
+
+  private CacheStats start = new CacheStats(0, 0, 0, 0, 0, 0);
+
+  private CacheStatsSubject(FailureMetadata failureMetadata, CacheStats stats) {
+    super(failureMetadata, stats);
+  }
+
+  public CacheStatsSubject since(CacheStats start) {
+    this.start = requireNonNull(start);
+    return this;
+  }
+
+  public void hasHitCount(int expectedHitCount) {
+    isNotNull();
+    check("hitCount()").that(actual().minus(start).hitCount()).isEqualTo(expectedHitCount);
+  }
+
+  public void hasMissCount(int expectedMissCount) {
+    isNotNull();
+    check("missCount()").that(actual().minus(start).missCount()).isEqualTo(expectedMissCount);
+  }
+}
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index bd9df30..9a839dd 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -18,28 +18,34 @@
 import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.collect.Iterables;
+import com.google.common.truth.CustomSubjectBuilder;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
 import java.util.List;
-import java.util.function.Function;
+import java.util.function.BiFunction;
 
 public class ListSubject<S extends Subject<S, E>, E> extends IterableSubject {
 
-  private final Function<E, S> elementAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator;
 
-  @SuppressWarnings("unchecked")
   public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat(
-      List<E> list, Function<E, S> elementAssertThatFunction) {
-    // The ListSubjectFactory always returns ListSubjects. -> Casting is appropriate.
-    return (ListSubject<S, E>)
-        assertAbout(new ListSubjectFactory<>(elementAssertThatFunction)).that(list);
+      List<E> list, Subject.Factory<S, E> subjectFactory) {
+    return assertAbout(elements()).thatCustom(list, subjectFactory);
+  }
+
+  public static CustomSubjectBuilder.Factory<ListSubjectBuilder> elements() {
+    return ListSubjectBuilder::new;
   }
 
   private ListSubject(
-      FailureMetadata failureMetadata, List<E> list, Function<E, S> elementAssertThatFunction) {
+      FailureMetadata failureMetadata,
+      List<E> list,
+      BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
     super(failureMetadata, list);
-    this.elementAssertThatFunction = elementAssertThatFunction;
+    this.elementSubjectCreator = elementSubjectCreator;
   }
 
   public S element(int index) {
@@ -49,20 +55,21 @@
     if (index >= list.size()) {
       failWithoutActual(fact("expected to have element at index", index));
     }
-    return elementAssertThatFunction.apply(list.get(index));
+    return elementSubjectCreator.apply(check("element(%s)", index), list.get(index));
   }
 
   public S onlyElement() {
     isNotNull();
     hasSize(1);
-    return element(0);
+    List<E> list = getActualList();
+    return elementSubjectCreator.apply(check("onlyElement()"), Iterables.getOnlyElement(list));
   }
 
   public S lastElement() {
     isNotNull();
     isNotEmpty();
     List<E> list = getActualList();
-    return element(list.size() - 1);
+    return elementSubjectCreator.apply(check("lastElement()"), Iterables.getLast(list));
   }
 
   @SuppressWarnings("unchecked")
@@ -78,20 +85,20 @@
     return (ListSubject<S, E>) super.named(s, objects);
   }
 
-  private static class ListSubjectFactory<S extends Subject<S, T>, T>
-      implements Subject.Factory<IterableSubject, Iterable<?>> {
+  public static class ListSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<T, S> elementAssertThatFunction;
-
-    ListSubjectFactory(Function<T, S> elementAssertThatFunction) {
-      this.elementAssertThatFunction = elementAssertThatFunction;
+    ListSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public ListSubject<S, T> createSubject(FailureMetadata failureMetadata, Iterable<?> objects) {
-      // The constructor of ListSubject only accepts lists. -> Casting is appropriate.
-      return new ListSubject<>(failureMetadata, (List<T>) objects, elementAssertThatFunction);
+    public <S extends Subject<S, E>, E> ListSubject<S, E> thatCustom(
+        List<E> list, Subject.Factory<S, E> subjectFactory) {
+      return that(list, (builder, element) -> builder.about(subjectFactory).that(element));
+    }
+
+    public <S extends Subject<S, E>, E> ListSubject<S, E> that(
+        List<E> list, BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
+      return new ListSubject<>(metadata(), list, elementSubjectCreator);
     }
   }
 }
diff --git a/java/com/google/gerrit/truth/OptionalSubject.java b/java/com/google/gerrit/truth/OptionalSubject.java
index d91f07b..b5fc5d0 100644
--- a/java/com/google/gerrit/truth/OptionalSubject.java
+++ b/java/com/google/gerrit/truth/OptionalSubject.java
@@ -17,41 +17,52 @@
 import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.truth.CustomSubjectBuilder;
 import com.google.common.truth.DefaultSubject;
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import java.util.Optional;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 public class OptionalSubject<S extends Subject<S, ? super T>, T>
     extends Subject<OptionalSubject<S, T>, Optional<T>> {
 
-  private final Function<? super T, ? extends S> valueAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator;
 
-  public static <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> assertThat(
+  // TODO(aliceks): Remove when all relevant usages are adapted to new check()/factory approach.
+  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
       Optional<T> optional, Function<? super T, ? extends S> elementAssertThatFunction) {
-    OptionalSubjectFactory<S, T> optionalSubjectFactory =
-        new OptionalSubjectFactory<>(elementAssertThatFunction);
-    return assertAbout(optionalSubjectFactory).that(optional);
+    Subject.Factory<S, T> valueSubjectFactory =
+        (metadata, value) -> elementAssertThatFunction.apply(value);
+    return assertThat(optional, valueSubjectFactory);
+  }
+
+  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
+      Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+    return assertAbout(optionals()).thatCustom(optional, valueSubjectFactory);
   }
 
   public static OptionalSubject<DefaultSubject, ?> assertThat(Optional<?> optional) {
-    // Unfortunately, we need to cast to DefaultSubject as Truth.assertThat()
+    // Unfortunately, we need to cast to DefaultSubject as StandardSubjectBuilder#that
     // only returns Subject<DefaultSubject, Object>. There shouldn't be a way
     // for that method not to return a DefaultSubject because the generic type
     // definitions of a Subject are quite strict.
-    Function<Object, DefaultSubject> valueAssertThatFunction =
-        value -> (DefaultSubject) Truth.assertThat(value);
-    return assertThat(optional, valueAssertThatFunction);
+    return assertAbout(optionals())
+        .that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+  }
+
+  public static CustomSubjectBuilder.Factory<OptionalSubjectBuilder> optionals() {
+    return OptionalSubjectBuilder::new;
   }
 
   private OptionalSubject(
       FailureMetadata failureMetadata,
       Optional<T> optional,
-      Function<? super T, ? extends S> valueAssertThatFunction) {
+      BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
     super(failureMetadata, optional);
-    this.valueAssertThatFunction = valueAssertThatFunction;
+    this.valueSubjectCreator = valueSubjectCreator;
   }
 
   public void isPresent() {
@@ -78,22 +89,28 @@
     isNotNull();
     isPresent();
     Optional<T> optional = actual();
-    return valueAssertThatFunction.apply(optional.get());
+    return valueSubjectCreator.apply(check("value()"), optional.get());
   }
 
-  private static class OptionalSubjectFactory<S extends Subject<S, ? super T>, T>
-      implements Subject.Factory<OptionalSubject<S, T>, Optional<T>> {
+  public static class OptionalSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<? super T, ? extends S> valueAssertThatFunction;
-
-    OptionalSubjectFactory(Function<? super T, ? extends S> valueAssertThatFunction) {
-      this.valueAssertThatFunction = valueAssertThatFunction;
+    OptionalSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @Override
-    public OptionalSubject<S, T> createSubject(
-        FailureMetadata failureMetadata, Optional<T> optional) {
-      return new OptionalSubject<>(failureMetadata, optional, valueAssertThatFunction);
+    public <S extends Subject<S, T>, T> OptionalSubject<S, T> thatCustom(
+        Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+      return that(optional, (builder, value) -> builder.about(valueSubjectFactory).that(value));
+    }
+
+    public OptionalSubject<DefaultSubject, ?> that(Optional<?> optional) {
+      return that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+    }
+
+    public <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> that(
+        Optional<T> optional,
+        BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
+      return new OptionalSubject<>(metadata(), optional, valueSubjectCreator);
     }
   }
 }
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index c94fc1d..b9b9bba 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -7,6 +7,7 @@
         "//java/com/google/gerrit/common:server",
         "//lib:args4j",
         "//lib:guava",
+        "//lib/auto:auto-value-annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 555abc3..1c16133 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -34,7 +34,9 @@
 
 package com.google.gerrit.util.cli;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.util.cli.Localizable.localizable;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ListMultimap;
@@ -45,8 +47,6 @@
 import com.google.inject.assistedinject.Assisted;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -68,7 +68,6 @@
 import org.kohsuke.args4j.ParserProperties;
 import org.kohsuke.args4j.spi.BooleanOptionHandler;
 import org.kohsuke.args4j.spi.EnumOptionHandler;
-import org.kohsuke.args4j.spi.FieldSetter;
 import org.kohsuke.args4j.spi.MethodSetter;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
@@ -286,7 +285,7 @@
   }
 
   public boolean wasHelpRequestedByOption() {
-    return parser.help.value;
+    return parser.help;
   }
 
   public void parseArgument(String... args) throws CmdLineException {
@@ -420,86 +419,30 @@
     throw new CmdLineException(parser, localizable("invalid boolean \"%s=%s\""), name, value);
   }
 
-  private static class PrefixedOption implements Option {
-    private final String prefix;
-    private final Option o;
-
-    PrefixedOption(String prefix, Option o) {
-      this.prefix = prefix;
-      this.o = o;
-    }
-
-    @Override
-    public String name() {
-      return getPrefixedName(prefix, o.name());
-    }
-
-    @Override
-    public String[] aliases() {
-      String[] prefixedAliases = new String[o.aliases().length];
-      for (int i = 0; i < prefixedAliases.length; i++) {
-        prefixedAliases[i] = getPrefixedName(prefix, o.aliases()[i]);
-      }
-      return prefixedAliases;
-    }
-
-    @Override
-    public String usage() {
-      return o.usage();
-    }
-
-    @Override
-    public String metaVar() {
-      return o.metaVar();
-    }
-
-    @Override
-    public boolean required() {
-      return o.required();
-    }
-
-    @Override
-    public boolean hidden() {
-      return o.hidden();
-    }
-
-    @SuppressWarnings("rawtypes")
-    @Override
-    public Class<? extends OptionHandler> handler() {
-      return o.handler();
-    }
-
-    @Override
-    public String[] depends() {
-      return o.depends();
-    }
-
-    @Override
-    public String[] forbids() {
-      return null;
-    }
-
-    @Override
-    public boolean help() {
-      return false;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return o.annotationType();
-    }
-
-    private static String getPrefixedName(String prefix, String name) {
-      return prefix + name;
-    }
+  private static Option newPrefixedOption(String prefix, Option o) {
+    requireNonNull(prefix);
+    checkArgument(o.name().startsWith("-"), "Option name must start with '-': %s", o);
+    String[] aliases = Arrays.stream(o.aliases()).map(prefix::concat).toArray(String[]::new);
+    return OptionUtil.newOption(
+        prefix + o.name(),
+        aliases,
+        o.usage(),
+        o.metaVar(),
+        o.required(),
+        false,
+        o.hidden(),
+        o.handler(),
+        o.depends(),
+        new String[0]);
   }
 
   public class MyParser extends org.kohsuke.args4j.CmdLineParser {
+    boolean help;
+
     @SuppressWarnings("rawtypes")
     private List<OptionHandler> optionsList;
 
     private Map<String, QueuedOption> queuedOptionsByName = new LinkedHashMap<>();
-    private HelpOption help;
 
     private class QueuedOption {
       public final Option option;
@@ -567,7 +510,7 @@
           Option o = m.getAnnotation(Option.class);
           if (o != null) {
             queueOption(
-                new PrefixedOption(prefix, o),
+                newPrefixedOption(prefix, o),
                 new MethodSetter(this, bean, m),
                 m.getAnnotation(RequiresOptions.class));
           }
@@ -576,7 +519,7 @@
           Option o = f.getAnnotation(Option.class);
           if (o != null) {
             queueOption(
-                new PrefixedOption(prefix, o),
+                newPrefixedOption(prefix, o),
                 Setters.create(f, bean),
                 f.getAnnotation(RequiresOptions.class));
           }
@@ -666,12 +609,33 @@
 
     private void ensureOptionsInitialized() {
       if (optionsList == null) {
-        help = new HelpOption();
         optionsList = new ArrayList<>();
-        addOption(help, help);
+        addOption(newHelpSetter(), newHelpOption());
       }
     }
 
+    private Setter<?> newHelpSetter() {
+      try {
+        return Setters.create(getClass().getDeclaredField("help"), this);
+      } catch (NoSuchFieldException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    private Option newHelpOption() {
+      return OptionUtil.newOption(
+          "--help",
+          new String[] {"-h"},
+          "display this help text",
+          "",
+          false,
+          false,
+          false,
+          BooleanOptionHandler.class,
+          new String[0],
+          new String[0]);
+    }
+
     private boolean isHandlerSpecified(OptionDef option) {
       return option.handler() != OptionHandler.class;
     }
@@ -685,90 +649,6 @@
     }
   }
 
-  private static class HelpOption implements Option, Setter<Boolean> {
-    private boolean value;
-
-    @Override
-    public String name() {
-      return "--help";
-    }
-
-    @Override
-    public String[] aliases() {
-      return new String[] {"-h"};
-    }
-
-    @Override
-    public String[] depends() {
-      return new String[] {};
-    }
-
-    @Override
-    public boolean hidden() {
-      return false;
-    }
-
-    @Override
-    public String usage() {
-      return "display this help text";
-    }
-
-    @Override
-    public void addValue(Boolean val) {
-      value = val;
-    }
-
-    @Override
-    public Class<? extends OptionHandler<Boolean>> handler() {
-      return BooleanOptionHandler.class;
-    }
-
-    @Override
-    public String metaVar() {
-      return "";
-    }
-
-    @Override
-    public boolean required() {
-      return false;
-    }
-
-    @Override
-    public Class<? extends Annotation> annotationType() {
-      return Option.class;
-    }
-
-    @Override
-    public FieldSetter asFieldSetter() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public AnnotatedElement asAnnotatedElement() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Class<Boolean> getType() {
-      return Boolean.class;
-    }
-
-    @Override
-    public boolean isMultiValued() {
-      return false;
-    }
-
-    @Override
-    public String[] forbids() {
-      return null;
-    }
-
-    @Override
-    public boolean help() {
-      return false;
-    }
-  }
-
   public CmdLineException reject(String message) {
     return new CmdLineException(parser, localizable(message));
   }
diff --git a/java/com/google/gerrit/util/cli/OptionHandlers.java b/java/com/google/gerrit/util/cli/OptionHandlers.java
index 84a0809..9547410 100644
--- a/java/com/google/gerrit/util/cli/OptionHandlers.java
+++ b/java/com/google/gerrit/util/cli/OptionHandlers.java
@@ -24,7 +24,7 @@
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.ParameterizedType;
-import java.util.Map.Entry;
+import java.util.Map;
 
 @Singleton
 public class OptionHandlers {
@@ -53,7 +53,7 @@
   private static ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> build(Injector i) {
     ImmutableMap.Builder<Class<?>, Provider<OptionHandlerFactory<?>>> map = ImmutableMap.builder();
     for (; i != null; i = i.getParent()) {
-      for (Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
+      for (Map.Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
         TypeLiteral<?> type = e.getKey().getTypeLiteral();
         if (type.getRawType() == OptionHandlerFactory.class
             && e.getKey().getAnnotation() == null
diff --git a/java/com/google/gerrit/util/cli/OptionUtil.java b/java/com/google/gerrit/util/cli/OptionUtil.java
new file mode 100644
index 0000000..1125a0d
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/OptionUtil.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.util.cli;
+
+import com.google.auto.value.AutoAnnotation;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.OptionHandler;
+
+/** Utilities to support creating new {@link Option} instances. */
+public class OptionUtil {
+  @AutoAnnotation
+  @SuppressWarnings("rawtypes")
+  public static Option newOption(
+      String name,
+      String[] aliases,
+      String usage,
+      String metaVar,
+      boolean required,
+      boolean help,
+      boolean hidden,
+      Class<? extends OptionHandler> handler,
+      String[] depends,
+      String[] forbids) {
+    return new AutoAnnotation_OptionUtil_newOption(
+        name, aliases, usage, metaVar, required, help, hidden, handler, depends, forbids);
+  }
+}
diff --git a/java/com/google/gwtorm/BUILD b/java/com/google/gwtorm/BUILD
new file mode 100644
index 0000000..e9cac34
--- /dev/null
+++ b/java/com/google/gwtorm/BUILD
@@ -0,0 +1,5 @@
+java_library(
+    name = "gwtorm",
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+)
diff --git a/java/com/google/gwtorm/client/CompoundKey.java b/java/com/google/gwtorm/client/CompoundKey.java
new file mode 100644
index 0000000..1c66d18
--- /dev/null
+++ b/java/com/google/gwtorm/client/CompoundKey.java
@@ -0,0 +1,108 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+import java.io.Serializable;
+
+/**
+ * Abstract key type composed of other keys.
+ *
+ * <p>Applications should subclass this type to create their own entity-specific key classes.
+ *
+ * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
+ */
+@SuppressWarnings("serial")
+public abstract class CompoundKey<P extends Key<?>> implements Key<P>, Serializable {
+  /** @return the member key components, minus the parent key. */
+  public abstract Key<?>[] members();
+
+  /** @return the parent key instance; null if this is a root level key. */
+  @Override
+  public P getParentKey() {
+    return null;
+  }
+
+  @Override
+  public int hashCode() {
+    int hc = 0;
+    if (getParentKey() != null) {
+      hc = getParentKey().hashCode();
+    }
+    for (final Key<?> k : members()) {
+      hc *= 31;
+      hc += k.hashCode();
+    }
+    return hc;
+  }
+
+  @Override
+  public boolean equals(final Object b) {
+    if (b == null || b.getClass() != getClass()) {
+      return false;
+    }
+
+    final CompoundKey<P> q = cast(b);
+    if (getParentKey() != null && !getParentKey().equals(q.getParentKey())) {
+      return false;
+    }
+
+    final Key<?>[] aMembers = members();
+    final Key<?>[] bMembers = q.members();
+    if (aMembers.length != bMembers.length) {
+      return false;
+    }
+    for (int i = 0; i < aMembers.length; i++) {
+      if (!aMembers[i].equals(bMembers[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    final StringBuffer r = new StringBuffer();
+    boolean first = true;
+    if (getParentKey() != null) {
+      r.append(KeyUtil.encode(getParentKey().toString()));
+      first = false;
+    }
+    for (final Key<?> k : members()) {
+      if (!first) {
+        r.append(',');
+      }
+      r.append(KeyUtil.encode(k.toString()));
+      first = false;
+    }
+    return r.toString();
+  }
+
+  @Override
+  public void fromString(final String in) {
+    final String[] parts = in.split(",");
+    int p = 0;
+    if (getParentKey() != null) {
+      getParentKey().fromString(KeyUtil.decode(parts[p++]));
+    }
+    for (final Key<?> k : members()) {
+      k.fromString(KeyUtil.decode(parts[p++]));
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <A extends Key<?>> CompoundKey<A> cast(final Object b) {
+    return (CompoundKey<A>) b;
+  }
+}
diff --git a/java/com/google/gwtorm/client/IntKey.java b/java/com/google/gwtorm/client/IntKey.java
new file mode 100644
index 0000000..08c90e0
--- /dev/null
+++ b/java/com/google/gwtorm/client/IntKey.java
@@ -0,0 +1,80 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+import java.io.Serializable;
+
+/**
+ * Abstract key type using a single integer value.
+ *
+ * <p>Applications should subclass this type to create their own entity-specific key classes.
+ *
+ * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
+ */
+@SuppressWarnings("serial")
+public abstract class IntKey<P extends Key<?>> implements Key<P>, Serializable {
+  /** @return id of the entity instance. */
+  public abstract int get();
+
+  /** @param newValue the new value of this key. */
+  protected abstract void set(int newValue);
+
+  /** @return the parent key instance; null if this is a root level key. */
+  @Override
+  public P getParentKey() {
+    return null;
+  }
+
+  @Override
+  public int hashCode() {
+    int hc = get();
+    if (getParentKey() != null) {
+      hc *= 31;
+      hc += getParentKey().hashCode();
+    }
+    return hc;
+  }
+
+  @Override
+  public boolean equals(final Object b) {
+    if (b == null || b.getClass() != getClass()) {
+      return false;
+    }
+
+    final IntKey<P> q = cast(b);
+    return get() == q.get() && KeyUtil.eq(getParentKey(), q.getParentKey());
+  }
+
+  @Override
+  public String toString() {
+    final StringBuffer r = new StringBuffer();
+    if (getParentKey() != null) {
+      r.append(getParentKey().toString());
+      r.append(',');
+    }
+    r.append(get());
+    return r.toString();
+  }
+
+  @Override
+  public void fromString(final String in) {
+    set(Integer.parseInt(KeyUtil.parseFromString(getParentKey(), in)));
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <A extends Key<?>> IntKey<A> cast(final Object b) {
+    return (IntKey<A>) b;
+  }
+}
diff --git a/java/com/google/gwtorm/client/Key.java b/java/com/google/gwtorm/client/Key.java
new file mode 100644
index 0000000..69a2248
--- /dev/null
+++ b/java/com/google/gwtorm/client/Key.java
@@ -0,0 +1,45 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+/**
+ * Generic type for an entity key.
+ *
+ * <p>Although not required, entities should make their primary key type implement this interface,
+ * permitting traversal up through the containment hierarchy of the entity keys.
+ *
+ * @param <P> type of the parent key. If no parent, use {@link Key} itself.
+ */
+public interface Key<P extends Key<?>> {
+  /**
+   * Get the parent key instance.
+   *
+   * @return the parent key; null if this entity key is a root-level key.
+   */
+  public P getParentKey();
+
+  @Override
+  public int hashCode();
+
+  @Override
+  public boolean equals(Object o);
+
+  /** @return the key, encoded in a string format . */
+  @Override
+  public String toString();
+
+  /** Reset this key instance to represent the data in the supplied string. */
+  public void fromString(String in);
+}
diff --git a/java/com/google/gwtorm/client/KeyUtil.java b/java/com/google/gwtorm/client/KeyUtil.java
new file mode 100644
index 0000000..e236d37
--- /dev/null
+++ b/java/com/google/gwtorm/client/KeyUtil.java
@@ -0,0 +1,91 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+/** Common utility functions for {@link Key} implementors. */
+public class KeyUtil {
+  private static Encoder ENCODER_IMPL = new StandardKeyEncoder();
+
+  /**
+   * Determine if two keys are equal, supporting null references.
+   *
+   * @param <T> type of the key entity.
+   * @param a first key to test; may be null.
+   * @param b second key to test; may be null.
+   * @return true if both <code>a</code> and <code>b</code> are null, or if both are not-null and
+   *     <code>a.equals(b)</code> is true. Otherwise false.
+   */
+  public static <T extends Key<?>> boolean eq(final T a, final T b) {
+    if (a == b) {
+      return true;
+    }
+    if (a == null || b == null) {
+      return false;
+    }
+    return a.equals(b);
+  }
+
+  /**
+   * Encode a string to be safe for use within a URL like string.
+   *
+   * <p>The returned encoded string has URL component characters escaped with hex escapes (e.g. ' '
+   * is '+' and '%' is '%25'). The special character '/' is left literal. The comma character (',')
+   * is always encoded, permitting multiple encoded string values to be joined together safely.
+   *
+   * @param e the string to encode, must not be null.
+   * @return the encoded string.
+   */
+  public static String encode(final String e) {
+    return ENCODER_IMPL.encode(e);
+  }
+
+  /**
+   * Decode a string previously encoded by {@link #encode(String)}.
+   *
+   * @param e the string to decode, must not be null.
+   * @return the decoded string.
+   */
+  public static String decode(final String e) {
+    return ENCODER_IMPL.decode(e);
+  }
+
+  /**
+   * Split a string along the last comma and parse into the parent.
+   *
+   * @param parent parent key; <code>parent.fromString(in[0..comma])</code>.
+   * @param in the input string.
+   * @return text (if any) after the last comma in the input.
+   */
+  public static String parseFromString(final Key<?> parent, final String in) {
+    final int comma = in.lastIndexOf(',');
+    if (comma < 0 && parent == null) {
+      return decode(in);
+    }
+    if (comma < 0 && parent != null) {
+      throw new IllegalArgumentException("Not enough components: " + in);
+    }
+    assert (parent != null);
+    parent.fromString(in.substring(0, comma));
+    return decode(in.substring(comma + 1));
+  }
+
+  public abstract static class Encoder {
+    public abstract String encode(String e);
+
+    public abstract String decode(String e);
+  }
+
+  private KeyUtil() {}
+}
diff --git a/java/com/google/gwtorm/client/StandardKeyEncoder.java b/java/com/google/gwtorm/client/StandardKeyEncoder.java
new file mode 100644
index 0000000..d6d503a
--- /dev/null
+++ b/java/com/google/gwtorm/client/StandardKeyEncoder.java
@@ -0,0 +1,111 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+import com.google.gwtorm.client.KeyUtil.Encoder;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+public class StandardKeyEncoder extends Encoder {
+  private static final char[] hexc = {
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+  };
+  private static final char safe[];
+  private static final byte hexb[];
+
+  static {
+    safe = new char[256];
+    safe['-'] = '-';
+    safe['_'] = '_';
+    safe['.'] = '.';
+    safe['!'] = '!';
+    safe['~'] = '~';
+    safe['*'] = '*';
+    safe['\''] = '\'';
+    safe['('] = '(';
+    safe[')'] = ')';
+    safe['/'] = '/';
+    safe[' '] = '+';
+    for (char c = '0'; c <= '9'; c++) safe[c] = c;
+    for (char c = 'A'; c <= 'Z'; c++) safe[c] = c;
+    for (char c = 'a'; c <= 'z'; c++) safe[c] = c;
+
+    hexb = new byte['f' + 1];
+    Arrays.fill(hexb, (byte) -1);
+    for (char i = '0'; i <= '9'; i++) hexb[i] = (byte) (i - '0');
+    for (char i = 'A'; i <= 'F'; i++) hexb[i] = (byte) ((i - 'A') + 10);
+    for (char i = 'a'; i <= 'f'; i++) hexb[i] = (byte) ((i - 'a') + 10);
+  }
+
+  @Override
+  public String encode(final String e) {
+    final byte[] b;
+    try {
+      b = e.getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+
+    final StringBuilder r = new StringBuilder(b.length);
+    for (int i = 0; i < b.length; i++) {
+      final int c = b[i] & 0xff;
+      final char s = safe[c];
+      if (s == 0) {
+        r.append('%');
+        r.append(hexc[c >> 4]);
+        r.append(hexc[c & 15]);
+      } else {
+        r.append(s);
+      }
+    }
+    return r.toString();
+  }
+
+  @Override
+  public String decode(final String e) {
+    if (e.indexOf('%') < 0) {
+      return e.replace('+', ' ');
+    }
+
+    final byte[] b = new byte[e.length()];
+    int bPtr = 0;
+    try {
+      for (int i = 0; i < e.length(); ) {
+        final char c = e.charAt(i);
+        if (c == '%' && i + 2 < e.length()) {
+          final int v = (hexb[e.charAt(i + 1)] << 4) | hexb[e.charAt(i + 2)];
+          if (v < 0) {
+            throw new IllegalArgumentException(e.substring(i, i + 3));
+          }
+          b[bPtr++] = (byte) v;
+          i += 3;
+        } else if (c == '+') {
+          b[bPtr++] = ' ';
+          i++;
+        } else {
+          b[bPtr++] = (byte) c;
+          i++;
+        }
+      }
+    } catch (ArrayIndexOutOfBoundsException err) {
+      throw new IllegalArgumentException("Bad encoding: " + e);
+    }
+    try {
+      return new String(b, 0, bPtr, "UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+  }
+}
diff --git a/java/com/google/gwtorm/client/StringKey.java b/java/com/google/gwtorm/client/StringKey.java
new file mode 100644
index 0000000..e56661f
--- /dev/null
+++ b/java/com/google/gwtorm/client/StringKey.java
@@ -0,0 +1,86 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+import java.io.Serializable;
+
+/**
+ * Abstract key type using a single string value.
+ *
+ * <p>Applications should subclass this type to create their own entity-specific key classes.
+ *
+ * @param <P> the parent key type. Use {@link Key} if no parent key is needed.
+ */
+@SuppressWarnings("serial")
+public abstract class StringKey<P extends Key<?>>
+    implements Key<P>, Serializable, Comparable<StringKey<?>> {
+  /** @return name of the entity instance. */
+  public abstract String get();
+
+  /** @param newValue the new value of this key. */
+  protected abstract void set(String newValue);
+
+  /** @return the parent key instance; null if this is a root level key. */
+  @Override
+  public P getParentKey() {
+    return null;
+  }
+
+  @Override
+  public int hashCode() {
+    int hc = get() != null ? get().hashCode() : 0;
+    if (getParentKey() != null) {
+      hc *= 31;
+      hc += getParentKey().hashCode();
+    }
+    return hc;
+  }
+
+  @Override
+  public boolean equals(final Object b) {
+    if (b == null || get() == null || b.getClass() != getClass()) {
+      return false;
+    }
+
+    final StringKey<P> q = cast(b);
+    return get().equals(q.get()) && KeyUtil.eq(getParentKey(), q.getParentKey());
+  }
+
+  @Override
+  public int compareTo(final StringKey<?> other) {
+    return get().compareTo(other.get());
+  }
+
+  @Override
+  public String toString() {
+    final StringBuffer r = new StringBuffer();
+    if (getParentKey() != null) {
+      r.append(getParentKey().toString());
+      r.append(',');
+    }
+    r.append(KeyUtil.encode(get()));
+    return r.toString();
+  }
+
+  @Override
+  public void fromString(final String in) {
+    set(KeyUtil.parseFromString(getParentKey(), in));
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <A extends Key<?>> StringKey<A> cast(final Object b) {
+    return (StringKey<A>) b;
+  }
+}
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index 8281d8e..f416f11 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -4,10 +4,10 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
-        "//lib:gwtorm",
         "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/prolog:runtime",
diff --git a/java/gerrit/PRED__load_commit_labels_1.java b/java/gerrit/PRED__load_commit_labels_1.java
index 1d0ba8a..693c89e 100644
--- a/java/gerrit/PRED__load_commit_labels_1.java
+++ b/java/gerrit/PRED__load_commit_labels_1.java
@@ -7,8 +7,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -36,27 +34,22 @@
     Term a1 = arg1.dereference();
 
     Term listHead = Prolog.Nil;
-    try {
-      ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
-      LabelTypes types = cd.getLabelTypes();
+    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+    LabelTypes types = cd.getLabelTypes();
 
-      for (PatchSetApproval a : cd.currentApprovals()) {
-        LabelType t = types.byLabel(a.getLabelId());
-        if (t == null) {
-          continue;
-        }
-
-        StructureTerm labelTerm =
-            new StructureTerm(
-                sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.getValue()));
-
-        StructureTerm userTerm =
-            new StructureTerm(sym_user, new IntegerTerm(a.getAccountId().get()));
-
-        listHead = new ListTerm(new StructureTerm(sym_commit_label, labelTerm, userTerm), listHead);
+    for (PatchSetApproval a : cd.currentApprovals()) {
+      LabelType t = types.byLabel(a.getLabelId());
+      if (t == null) {
+        continue;
       }
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
+
+      StructureTerm labelTerm =
+          new StructureTerm(
+              sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.getValue()));
+
+      StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.getAccountId().get()));
+
+      listHead = new ListTerm(new StructureTerm(sym_commit_label, labelTerm, userTerm), listHead);
     }
 
     if (!a1.unify(listHead, engine.trail)) {
diff --git a/java/gerrit/PRED_get_legacy_label_types_1.java b/java/gerrit/PRED_get_legacy_label_types_1.java
index ef79e05..2f0c1ea 100644
--- a/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -53,12 +51,7 @@
   public Operation exec(Prolog engine) throws PrologException {
     engine.setB0();
     Term a1 = arg1.dereference();
-    List<LabelType> list;
-    try {
-      list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
-    }
+    List<LabelType> list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
     Term head = Prolog.Nil;
     for (int idx = list.size() - 1; 0 <= idx; idx--) {
       head = new ListTerm(export(list.get(idx)), head);
diff --git a/java/gerrit/PRED_pure_revert_1.java b/java/gerrit/PRED_pure_revert_1.java
index 95a0729..6300a668 100644
--- a/java/gerrit/PRED_pure_revert_1.java
+++ b/java/gerrit/PRED_pure_revert_1.java
@@ -15,8 +15,6 @@
 package gerrit;
 
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -36,12 +34,7 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    Boolean isPureRevert;
-    try {
-      isPureRevert = StoredValues.CHANGE_DATA.get(engine).isPureRevert();
-    } catch (OrmException e) {
-      throw new JavaException(this, 1, e);
-    }
+    Boolean isPureRevert = StoredValues.CHANGE_DATA.get(engine).isPureRevert();
     if (!a1.unify(new IntegerTerm(Boolean.TRUE.equals(isPureRevert) ? 1 : 0), engine.trail)) {
       return engine.fail();
     }
diff --git a/java/gerrit/PRED_unresolved_comments_count_1.java b/java/gerrit/PRED_unresolved_comments_count_1.java
index 5ed1525..d4abcc54 100644
--- a/java/gerrit/PRED_unresolved_comments_count_1.java
+++ b/java/gerrit/PRED_unresolved_comments_count_1.java
@@ -15,8 +15,6 @@
 package gerrit;
 
 import com.google.gerrit.server.rules.StoredValues;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -35,13 +33,9 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    try {
-      Integer count = StoredValues.CHANGE_DATA.get(engine).unresolvedCommentCount();
-      if (!a1.unify(new IntegerTerm(count != null ? count : 0), engine.trail)) {
-        return engine.fail();
-      }
-    } catch (OrmException err) {
-      throw new JavaException(this, 1, err);
+    Integer count = StoredValues.CHANGE_DATA.get(engine).unresolvedCommentCount();
+    if (!a1.unify(new IntegerTerm(count != null ? count : 0), engine.trail)) {
+      return engine.fail();
     }
     return cont;
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 6dc03ae..6fc1984 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -47,6 +47,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
 import com.google.common.truth.Correspondence;
+import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -66,6 +67,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
@@ -107,13 +109,10 @@
 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.AccountManager;
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.account.ProjectWatches;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
@@ -125,6 +124,7 @@
 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.Sequences;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -136,7 +136,6 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -224,8 +223,6 @@
   @Inject
   private DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners;
 
-  @Inject private AccountManager accountManager;
-
   @Inject protected GroupOperations groupOperations;
 
   private AccountIndexedCounter accountIndexedCounter;
@@ -317,19 +314,19 @@
   private Account.Id createByAccountCreator(int expectedAccountReindexCalls) throws Exception {
     String name = "foo";
     TestAccount foo = accountCreator.create(name);
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
     assertThat(info.username).isEqualTo(name);
     assertThat(info.name).isEqualTo(name);
     accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
-    assertUserBranch(foo.getId(), name, null);
-    return foo.getId();
+    assertUserBranch(foo.id(), name, null);
+    return foo.id();
   }
 
   @Test
   public void createAnonymousCowardByAccountCreator() throws Exception {
     TestAccount anonymousCoward = accountCreator.create();
     accountIndexedCounter.assertReindexOf(anonymousCoward);
-    assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
+    assertUserBranchWithoutAccountConfig(anonymousCoward.id());
   }
 
   @Test
@@ -356,10 +353,10 @@
   @Test
   public void createAccountUsernameAlreadyTaken() throws Exception {
     AccountInput input = new AccountInput();
-    input.username = admin.username;
+    input.username = admin.username();
 
     exception.expect(ResourceConflictException.class);
-    exception.expectMessage("username '" + admin.username + "' already exists");
+    exception.expectMessage("username '" + admin.username() + "' already exists");
     gApi.accounts().create(input);
   }
 
@@ -367,10 +364,10 @@
   public void createAccountEmailAlreadyTaken() throws Exception {
     AccountInput input = new AccountInput();
     input.username = "foo";
-    input.email = admin.email;
+    input.email = admin.email();
 
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("email '" + admin.email + "' already exists");
+    exception.expectMessage("email '" + admin.email() + "' already exists");
     gApi.accounts().create(input);
   }
 
@@ -442,18 +439,18 @@
   @Test
   public void updateAccountWithoutAccountConfigNoteDb() throws Exception {
     TestAccount anonymousCoward = accountCreator.create();
-    assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
+    assertUserBranchWithoutAccountConfig(anonymousCoward.id());
 
     String status = "OOO";
     Optional<AccountState> accountState =
         accountsUpdateProvider
             .get()
-            .update("Set status", anonymousCoward.getId(), u -> u.setStatus(status));
+            .update("Set status", anonymousCoward.id(), u -> u.setStatus(status));
     assertThat(accountState).isPresent();
     Account account = accountState.get().getAccount();
     assertThat(account.getFullName()).isNull();
     assertThat(account.getStatus()).isEqualTo(status);
-    assertUserBranch(anonymousCoward.getId(), null, status);
+    assertUserBranch(anonymousCoward.id(), null, status);
   }
 
   private void assertUserBranchWithoutAccountConfig(Account.Id accountId) throws Exception {
@@ -520,12 +517,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);
   }
@@ -624,16 +636,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
@@ -648,7 +661,7 @@
     assertThat(change.stars).contains(DEFAULT_LABEL);
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     gApi.accounts().self().unstarChange(triplet);
     change = info(triplet);
@@ -656,7 +669,7 @@
     assertThat(change.stars).isNull();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     accountIndexedCounter.assertNoReindex();
   }
@@ -687,7 +700,7 @@
     assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     gApi.accounts()
         .self()
@@ -706,14 +719,14 @@
     assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
     refUpdateCounter.assertRefUpdateFor(
         RefUpdateCounter.projectRef(
-            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
+            allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id())));
 
     accountIndexedCounter.assertNoReindex();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to get stars of another account");
-    gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
+    gApi.accounts().id(Integer.toString((admin.id().get()))).getStars(triplet);
   }
 
   @Test
@@ -766,22 +779,22 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     in = new AddReviewerInput();
-    in.reviewer = user2.email;
+    in.reviewer = user2.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).abandon();
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress);
+    assertThat(messages.get(0).rcpt()).containsExactly(user2.getEmailAddress());
     accountIndexedCounter.assertNoReindex();
   }
 
@@ -789,20 +802,20 @@
   public void addReviewerToIgnoredChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message message = messages.get(0);
-    assertThat(message.rcpt()).containsExactly(user.emailAddress);
-    assertMailReplyTo(message, admin.email);
+    assertThat(message.rcpt()).containsExactly(user.getEmailAddress());
+    assertMailReplyTo(message, admin.email());
     accountIndexedCounter.assertNoReindex();
   }
 
@@ -829,20 +842,20 @@
     TestAccount foo = accountCreator.create(username, email, name);
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
     String status = "OOO";
-    gApi.accounts().id(foo.id.get()).setStatus(status);
+    gApi.accounts().id(foo.id().get()).setStatus(status);
 
-    requestScopeOperations.setApiUser(foo.getId());
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
-    assertThat(detail._accountId).isEqualTo(foo.id.get());
+    requestScopeOperations.setApiUser(foo.id());
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
+    assertThat(detail._accountId).isEqualTo(foo.id().get());
     assertThat(detail.name).isEqualTo(name);
     assertThat(detail.username).isEqualTo(username);
     assertThat(detail.email).isEqualTo(email);
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
     assertThat(detail.status).isEqualTo(status);
-    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.getId()).getRegisteredOn());
+    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).getRegisteredOn());
     assertThat(detail.inactive).isNull();
     assertThat(detail._moreAccounts).isNull();
   }
@@ -854,10 +867,10 @@
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
-    requestScopeOperations.setApiUser(user.getId());
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
+    requestScopeOperations.setApiUser(user.id());
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
     assertThat(detail.secondaryEmails).isNull();
   }
 
@@ -867,9 +880,9 @@
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
-    AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
+    AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
   }
 
@@ -878,15 +891,15 @@
     String email = "preferred@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    requestScopeOperations.setApiUser(foo.getId());
+    requestScopeOperations.setApiUser(foo.id());
     assertThat(getEmails()).containsExactly(email);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.hashCode()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
-    requestScopeOperations.setApiUser(foo.getId());
+    requestScopeOperations.setApiUser(foo.id());
     assertThat(getEmails()).containsExactly(email, secondaryEmail);
   }
 
@@ -895,10 +908,10 @@
     String email = "preferred2@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(foo.id.get()).getEmails();
+    gApi.accounts().id(foo.id().get()).getEmails();
   }
 
   @Test
@@ -907,13 +920,10 @@
     String secondaryEmail = "secondary3@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     EmailInput input = newEmailInput(secondaryEmail);
-    gApi.accounts().id(foo.id.hashCode()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
 
     assertThat(
-            gApi.accounts()
-                .id(foo.id.get())
-                .getEmails()
-                .stream()
+            gApi.accounts().id(foo.id().get()).getEmails().stream()
                 .map(e -> e.email)
                 .collect(toSet()))
         .containsExactly(email, secondaryEmail);
@@ -931,7 +941,7 @@
     }
 
     requestScopeOperations.resetCurrentApiUser();
-    assertThat(getEmails()).containsAllIn(emails);
+    assertThat(getEmails()).containsAtLeastElementsIn(emails);
   }
 
   @Test
@@ -965,9 +975,9 @@
   public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
     TestAccount account = accountCreator.create(name("user"));
     EmailInput input = newEmailInput("test@test.com");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(account.username).addEmail(input);
+    gApi.accounts().id(account.username()).addEmail(input);
   }
 
   @Test
@@ -977,7 +987,7 @@
     gApi.accounts().self().addEmail(input);
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("Identity 'mailto:" + email + "' in use by another account");
-    gApi.accounts().id(user.username).addEmail(input);
+    gApi.accounts().id(user.username()).addEmail(input);
   }
 
   @Test
@@ -1000,7 +1010,7 @@
       value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     String email = "self@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -1011,11 +1021,11 @@
   public void cannotAddEmailToBeConfirmedToOtherAccountWithoutModifyAccountPermission()
       throws Exception {
     TestAccount user = accountCreator.create();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.id.get()).addEmail(newEmailInput("foo@example.com", false));
+    gApi.accounts().id(admin.id().get()).addEmail(newEmailInput("foo@example.com", false));
   }
 
   @Test
@@ -1025,7 +1035,7 @@
   public void addEmailToBeConfirmedToOtherAccount() throws Exception {
     TestAccount user = accountCreator.create();
     String email = "me@example.com";
-    gApi.accounts().id(user.id.get()).addEmail(newEmailInput(email, false));
+    gApi.accounts().id(user.id().get()).addEmail(newEmailInput(email, false));
   }
 
   @Test
@@ -1054,16 +1064,17 @@
         .get()
         .update(
             "Add External IDs",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
-                        ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email))
+                        ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id(), email))
                     .addExternalId(
-                        ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email)));
+                        ExternalId.createWithEmail(
+                            ExternalId.Key.parse(extId2), admin.id(), email)));
     accountIndexedCounter.assertReindexOf(admin);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
-        .containsAllOf(extId1, extId2);
+        .containsAtLeast(extId1, extId2);
 
     requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
@@ -1084,30 +1095,30 @@
     EmailInput input = new EmailInput();
     input.email = email;
     input.noConfirmation = true;
-    gApi.accounts().id(user.id.get()).addEmail(input);
+    gApi.accounts().id(user.id().get()).addEmail(input);
     accountIndexedCounter.assertReindexOf(user);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(getEmails()).contains(email);
 
     // admin can delete email of user
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.accounts().id(user.id.get()).deleteEmail(email);
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.accounts().id(user.id().get()).deleteEmail(email);
     accountIndexedCounter.assertReindexOf(user);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(getEmails()).doesNotContain(email);
 
     // user cannot delete email of admin
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
+    gApi.accounts().id(admin.id().get()).deleteEmail(admin.email());
   }
 
   @Test
   public void lookUpByEmail() throws Exception {
     // exact match with scheme "mailto:"
-    assertEmail(emails.getAccountFor(admin.email), admin);
+    assertEmail(emails.getAccountFor(admin.email()), admin);
 
     // exact match with other scheme
     String email = "foo.bar@example.com";
@@ -1115,26 +1126,28 @@
         .get()
         .update(
             "Add Email",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email)));
+                    ExternalId.createWithEmail(
+                        ExternalId.Key.parse("foo:bar"), admin.id(), email)));
     assertEmail(emails.getAccountFor(email), admin);
 
     // wrong case doesn't match
-    assertThat(emails.getAccountFor(admin.email.toUpperCase(Locale.US))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email().toUpperCase(Locale.US))).isEmpty();
 
     // prefix doesn't match
-    assertThat(emails.getAccountFor(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email().substring(0, admin.email().indexOf('@'))))
+        .isEmpty();
 
     // non-existing doesn't match
     assertThat(emails.getAccountFor("non-existing@example.com")).isEmpty();
 
     // lookup several accounts by email at once
     ImmutableSetMultimap<String, Account.Id> byEmails =
-        emails.getAccountsFor(admin.email, user.email);
-    assertEmail(byEmails.get(admin.email), admin);
-    assertEmail(byEmails.get(user.email), user);
+        emails.getAccountsFor(admin.email(), user.email());
+    assertEmail(byEmails.get(admin.email()), admin);
+    assertEmail(byEmails.get(user.email()), user);
   }
 
   @Test
@@ -1145,12 +1158,12 @@
     TestAccount foo = accountCreator.create(name("foo"));
     accountsUpdateProvider
         .get()
-        .update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(prefEmail));
+        .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(prefEmail));
 
     // verify that the account is still found when using the preferred email to lookup the account
     ImmutableSet<Account.Id> accountsByPrefEmail = emails.getAccountFor(prefEmail);
     assertThat(accountsByPrefEmail).hasSize(1);
-    assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id);
+    assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id());
 
     // look up by email prefix doesn't find the account
     accountsByPrefEmail = emails.getAccountFor(prefix);
@@ -1186,31 +1199,31 @@
 
   @Test
   public void adminCanSetNameOfOtherUser() throws Exception {
-    gApi.accounts().id(user.username).setName("User McUserface");
-    assertThat(gApi.accounts().id(user.username).get().name).isEqualTo("User McUserface");
+    gApi.accounts().id(user.username()).setName("User McUserface");
+    assertThat(gApi.accounts().id(user.username()).get().name).isEqualTo("User McUserface");
   }
 
   @Test
   public void userCannotSetNameOfOtherUser() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    gApi.accounts().id(admin.username).setName("Admin McAdminface");
+    gApi.accounts().id(admin.username()).setName("Admin McAdminface");
   }
 
   @Test
   @Sandboxed
   public void userCanSetNameOfOtherUserWithModifyAccountPermission() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.MODIFY_ACCOUNT);
-    gApi.accounts().id(admin.username).setName("Admin McAdminface");
-    assertThat(gApi.accounts().id(admin.username).get().name).isEqualTo("Admin McAdminface");
+    gApi.accounts().id(admin.username()).setName("Admin McAdminface");
+    assertThat(gApi.accounts().id(admin.username()).get().name).isEqualTo("Admin McAdminface");
   }
 
   @Test
   public void fetchUserBranch() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
-    String userRefName = RefNames.refsUsers(user.id);
+    String userRefName = RefNames.refsUsers(user.id());
 
     // remove default READ permissions
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
@@ -1253,7 +1266,7 @@
     accountIndexedCounter.assertNoReindex();
 
     // fetching user branch of another user fails
-    String otherUserRefName = RefNames.refsUsers(admin.id);
+    String otherUserRefName = RefNames.refsUsers(admin.id());
     exception.expect(TransportException.class);
     exception.expectMessage("Remote does not have " + otherUserRefName + " available for fetch.");
     fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
@@ -1262,24 +1275,24 @@
   @Test
   public void pushToUserBranch() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(admin.getIdent(), allUsersRepo);
-    push.to(RefNames.refsUsers(admin.id)).assertOkStatus();
+    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+    push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(admin.getIdent(), allUsersRepo);
+    push = pushFactory.create(admin.newIdent(), allUsersRepo);
     push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
     accountIndexedCounter.assertReindexOf(admin);
   }
 
   @Test
   public void pushToUserBranchForReview() throws Exception {
-    String userRefName = RefNames.refsUsers(admin.id);
+    String userRefName = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRefName + ":userRef");
     allUsersRepo.reset("userRef");
-    PushOneCommit push = pushFactory.create(admin.getIdent(), allUsersRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
     PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1288,7 +1301,7 @@
     gApi.changes().id(r.getChangeId()).current().submit();
     accountIndexedCounter.assertReindexOf(admin);
 
-    push = pushFactory.create(admin.getIdent(), allUsersRepo);
+    push = pushFactory.create(admin.newIdent(), allUsersRepo);
     r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
     r.assertOkStatus();
     accountIndexedCounter.assertNoReindex();
@@ -1300,7 +1313,7 @@
 
   @Test
   public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1311,7 +1324,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1326,8 +1339,8 @@
     accountIndexedCounter.assertReindexOf(admin);
 
     AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(admin.email);
-    assertThat(info.name).isEqualTo(admin.fullName);
+    assertThat(info.email).isEqualTo(admin.email());
+    assertThat(info.name).isEqualTo(admin.fullName());
     assertThat(info.status).isEqualTo("out-of-office");
   }
 
@@ -1335,7 +1348,7 @@
   public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
       throws Exception {
     TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id);
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
@@ -1349,7 +1362,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                foo.getIdent(),
+                foo.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1359,7 +1372,7 @@
     accountIndexedCounter.assertNoReindex();
     assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
 
-    requestScopeOperations.setApiUser(foo.getId());
+    requestScopeOperations.setApiUser(foo.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).current().submit();
 
@@ -1367,13 +1380,13 @@
 
     AccountInfo info = gApi.accounts().self().get();
     assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    assertThat(info.name).isEqualTo(foo.fullName());
   }
 
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1381,7 +1394,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1399,7 +1412,7 @@
                 + " Invalid config file %s in commit %s",
             r.getCommit().name(),
             AccountProperties.ACCOUNT_CONFIG,
-            admin.id,
+            admin.id(),
             AccountProperties.ACCOUNT_CONFIG,
             r.getCommit().name()));
     gApi.changes().id(r.getChangeId()).current().submit();
@@ -1408,7 +1421,7 @@
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1420,7 +1433,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1435,14 +1448,14 @@
     exception.expectMessage(
         String.format(
             "invalid account configuration: invalid preferred email '%s' for account '%s'",
-            noEmail, admin.id));
+            noEmail, admin.id()));
     gApi.changes().id(r.getChangeId()).current().submit();
   }
 
   @Test
   public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
       throws Exception {
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1453,7 +1466,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1474,8 +1487,8 @@
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
     TestAccount foo = accountCreator.create(name("foo"));
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
-    String userRef = RefNames.refsUsers(foo.id);
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1492,7 +1505,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1506,13 +1519,13 @@
     gApi.changes().id(r.getChangeId()).current().submit();
     accountIndexedCounter.assertReindexOf(foo);
 
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
   }
 
   @Test
   public void pushWatchConfigToUserBranch() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config wc = new Config();
@@ -1523,7 +1536,7 @@
         ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Add project watch",
             ProjectWatches.WATCH_CONFIG,
@@ -1536,7 +1549,7 @@
         ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
     push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Add invalid project watch",
             ProjectWatches.WATCH_CONFIG,
@@ -1546,17 +1559,17 @@
     r.assertMessage(
         String.format(
             "%s: Invalid project watch of account %d for project %s: %s",
-            ProjectWatches.WATCH_CONFIG, admin.getId().get(), project.get(), invalidNotifyValue));
+            ProjectWatches.WATCH_CONFIG, admin.id().get(), project.get(), invalidNotifyValue));
   }
 
   @Test
   public void pushAccountConfigToUserBranch() throws Exception {
     TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
-    requestScopeOperations.setApiUser(oooUser.getId());
+    requestScopeOperations.setApiUser(oooUser.id());
 
     // Must clone as oooUser to ensure the push is allowed.
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
-    fetch(allUsersRepo, RefNames.refsUsers(oooUser.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config ac = getAccountConfig(allUsersRepo);
@@ -1565,32 +1578,32 @@
     accountIndexedCounter.clear();
     pushFactory
         .create(
-            oooUser.getIdent(),
+            oooUser.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
             ac.toText())
-        .to(RefNames.refsUsers(oooUser.id))
+        .to(RefNames.refsUsers(oooUser.id()))
         .assertOkStatus();
 
     accountIndexedCounter.assertReindexOf(oooUser);
 
     AccountInfo info = gApi.accounts().self().get();
-    assertThat(info.email).isEqualTo(oooUser.email);
-    assertThat(info.name).isEqualTo(oooUser.fullName);
+    assertThat(info.email).isEqualTo(oooUser.email());
+    assertThat(info.name).isEqualTo(oooUser.fullName());
     assertThat(info.status).isEqualTo("out-of-office");
   }
 
   @Test
   public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1603,7 +1616,7 @@
                 + " Invalid config file %s in commit %s",
             r.getCommit().name(),
             AccountProperties.ACCOUNT_CONFIG,
-            admin.id,
+            admin.id(),
             AccountProperties.ACCOUNT_CONFIG,
             r.getCommit().name()));
     accountIndexedCounter.assertNoReindex();
@@ -1612,7 +1625,7 @@
   @Test
   public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     String noEmail = "no.email";
@@ -1622,7 +1635,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1630,19 +1643,19 @@
             .to(RefNames.REFS_USERS_SELF);
     r.assertErrorStatus("invalid account configuration");
     r.assertMessage(
-        String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id));
+        String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id()));
     accountIndexedCounter.assertNoReindex();
   }
 
   @Test
   public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
     TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id);
+    String userRef = RefNames.refsUsers(foo.id());
 
     String noEmail = "no.email";
     accountsUpdateProvider
         .get()
-        .update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(noEmail));
+        .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
@@ -1656,7 +1669,7 @@
 
     pushFactory
         .create(
-            foo.getIdent(),
+            foo.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1665,16 +1678,16 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
     assertThat(info.email).isEqualTo(noEmail);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    assertThat(info.name).isEqualTo(foo.fullName());
     assertThat(info.status).isEqualTo(status);
   }
 
   @Test
   public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
     TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-    String userRef = RefNames.refsUsers(foo.id);
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1689,7 +1702,7 @@
 
     pushFactory
         .create(
-            foo.getIdent(),
+            foo.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1698,15 +1711,15 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    AccountInfo info = gApi.accounts().id(foo.id().get()).get();
     assertThat(info.email).isEqualTo(email);
-    assertThat(info.name).isEqualTo(foo.fullName);
+    assertThat(info.name).isEqualTo(foo.fullName());
   }
 
   @Test
   public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
     allUsersRepo.reset("userRef");
 
     Config ac = getAccountConfig(allUsersRepo);
@@ -1715,7 +1728,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 allUsersRepo,
                 "Update account config",
                 AccountProperties.ACCOUNT_CONFIG,
@@ -1731,8 +1744,8 @@
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
     TestAccount foo = accountCreator.create(name("foo"));
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
-    String userRef = RefNames.refsUsers(foo.id);
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+    String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
@@ -1746,7 +1759,7 @@
 
     pushFactory
         .create(
-            admin.getIdent(),
+            admin.newIdent(),
             allUsersRepo,
             "Update account config",
             AccountProperties.ACCOUNT_CONFIG,
@@ -1755,7 +1768,7 @@
         .assertOkStatus();
     accountIndexedCounter.assertReindexOf(foo);
 
-    assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
+    assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
   }
 
   @Test
@@ -1765,7 +1778,7 @@
 
     String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.getIdent(), allUsersRepo).to(userRef);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create user branch.");
 
@@ -1782,7 +1795,7 @@
 
     String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    pushFactory.create(admin.getIdent(), allUsersRepo).to(userRef).assertOkStatus();
+    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
       assertThat(repo.exactRef(userRef)).isNotNull();
@@ -1798,7 +1811,7 @@
 
     String userRef = RefNames.REFS_USERS + "foo";
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.getIdent(), allUsersRepo).to(userRef);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
 
@@ -1818,7 +1831,7 @@
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     pushFactory
-        .create(admin.getIdent(), allUsersRepo)
+        .create(admin.newIdent(), allUsersRepo)
         .to(RefNames.REFS_USERS_DEFAULT)
         .assertOkStatus();
 
@@ -1837,7 +1850,7 @@
         REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     PushResult r = deleteRef(allUsersRepo, userRef);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
     assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
@@ -1859,7 +1872,7 @@
         REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    String userRef = RefNames.refsUsers(admin.id);
+    String userRef = RefNames.refsUsers(admin.id());
     PushResult r = deleteRef(allUsersRepo, userRef);
     RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
     assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
@@ -1868,8 +1881,8 @@
       assertThat(repo.exactRef(userRef)).isNull();
     }
 
-    assertThat(accountCache.get(admin.id)).isEmpty();
-    assertThat(accountQueryProvider.get().byDefault(admin.id.toString())).isEmpty();
+    assertThat(accountCache.get(admin.id())).isEmpty();
+    assertThat(accountQueryProvider.get().byDefault(admin.id().toString())).isEmpty();
   }
 
   @Test
@@ -1881,7 +1894,7 @@
     assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
     assertKeys(key);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     exception.expectMessage(id);
     gApi.accounts().self().gpgKey(id).get();
@@ -1912,13 +1925,13 @@
         .get()
         .update(
             "Add External ID",
-            user.getId(),
-            u -> u.addExternalId(ExternalId.create("foo", "myId", user.getId())));
+            user.id(),
+            u -> u.addExternalId(ExternalId.create("foo", "myId", user.id())));
     accountIndexedCounter.assertReindexOf(user);
 
     TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("GPG key already associated with another account");
@@ -2011,12 +2024,12 @@
     assertSequenceNumbers(info);
     SshKeyInfo key = info.get(0);
     KeyPair keyPair = sshKeys.getKeyPair(admin);
-    String inital = TestSshKeys.publicKey(keyPair, admin.email);
+    String inital = TestSshKeys.publicKey(keyPair, admin.email());
     assertThat(key.sshPublicKey).isEqualTo(inital);
     accountIndexedCounter.assertNoReindex();
 
     // Add a new key
-    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
+    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
     gApi.accounts().self().addSshKey(newKey);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
@@ -2031,7 +2044,7 @@
     accountIndexedCounter.assertNoReindex();
 
     // Add another new key
-    String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
+    String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
     gApi.accounts().self().addSshKey(newKey2);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(3);
@@ -2048,7 +2061,7 @@
 
     // Mark first key as invalid
     assertThat(info.get(0).valid).isTrue();
-    authorizedKeys.markKeyInvalid(admin.id, 1);
+    authorizedKeys.markKeyInvalid(admin.id(), 1);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
     assertThat(info.get(0).seq).isEqualTo(1);
@@ -2061,19 +2074,19 @@
   @Test
   public void reindexPermissions() throws Exception {
     // admin can reindex any account
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.accounts().id(user.username).index();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.accounts().id(user.username()).index();
     accountIndexedCounter.assertReindexOf(user);
 
     // user can reindex own account
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().index();
     accountIndexedCounter.assertReindexOf(user);
 
     // user cannot reindex any account
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
-    gApi.accounts().id(admin.username).index();
+    gApi.accounts().id(admin.username()).index();
   }
 
   @Test
@@ -2099,13 +2112,13 @@
         .get()
         .update(
             "Delete External ID",
-            account.getId(),
-            u -> u.deleteExternalId(ExternalId.createEmail(account.getId(), email)));
+            account.id(),
+            u -> u.deleteExternalId(ExternalId.createEmail(account.id(), email)));
     expectedProblems.add(
         new ConsistencyProblemInfo(
             ConsistencyProblemInfo.Status.ERROR,
             "Account '"
-                + account.getId().get()
+                + account.id().get()
                 + "' has no external ID for its preferred email '"
                 + email
                 + "'"));
@@ -2121,11 +2134,11 @@
     assertThat(accountQueryProvider.get().byDefault(name)).isEmpty();
 
     TestAccount foo1 = accountCreator.create(name + "-1");
-    assertThat(gApi.accounts().id(foo1.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
 
     TestAccount foo2 = accountCreator.create(name + "-2");
-    gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    gApi.accounts().id(foo2.username()).setActive(false);
+    assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
 
     assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
   }
@@ -2133,8 +2146,8 @@
   @Test
   public void checkMetaId() throws Exception {
     // metaId is set when account is loaded
-    assertThat(accounts.get(admin.getId()).get().getAccount().getMetaId())
-        .isEqualTo(getMetaId(admin.getId()));
+    assertThat(accounts.get(admin.id()).get().getAccount().getMetaId())
+        .isEqualTo(getMetaId(admin.id()));
 
     // metaId is set when account is created
     AccountsUpdate au = accountsUpdateProvider.get();
@@ -2173,7 +2186,7 @@
 
   @Test
   public void allGroupsForAnAdminAccountCanBeRetrieved() throws Exception {
-    List<GroupInfo> groups = gApi.accounts().id(admin.username).getGroups();
+    List<GroupInfo> groups = gApi.accounts().id(admin.username()).getGroups();
     assertThat(groups)
         .comparingElementsUsing(getGroupToNameCorrespondence())
         .containsExactly("Anonymous Users", "Registered Users", "Administrators");
@@ -2261,7 +2274,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2275,20 +2288,20 @@
                 try {
                   accountsUpdateProvider
                       .get()
-                      .update("Set Status", admin.id, u -> u.setStatus(status));
-                } catch (IOException | ConfigInvalidException | OrmException e) {
+                      .update("Set Status", admin.id(), u -> u.setStatus(status));
+                } catch (IOException | ConfigInvalidException | StorageException e) {
                   // Ignore, the successful update of the account is asserted later
                 }
               }
             },
             Runnables.doNothing());
     assertThat(doneBgUpdate.get()).isFalse();
-    AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
+    AccountInfo accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
     Optional<AccountState> updatedAccountState =
-        update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
+        update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
     assertThat(doneBgUpdate.get()).isTrue();
 
     assertThat(updatedAccountState).isPresent();
@@ -2296,7 +2309,7 @@
     assertThat(updatedAccount.getStatus()).isEqualTo(status);
     assertThat(updatedAccount.getFullName()).isEqualTo(fullName);
 
-    accountInfo = gApi.accounts().id(admin.id.get()).get();
+    accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(status);
     assertThat(accountInfo.name).isEqualTo(fullName);
   }
@@ -2311,7 +2324,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2331,38 +2344,38 @@
                     .get()
                     .update(
                         "Set Status",
-                        admin.id,
+                        admin.id(),
                         u -> u.setStatus(status.get(bgCounter.getAndAdd(1))));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             },
             Runnables.doNothing());
     assertThat(bgCounter.get()).isEqualTo(0);
-    AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
+    AccountInfo accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
     try {
-      update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
+      update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
       fail("expected LockFailureException");
     } catch (LockFailureException e) {
       // Ignore, expected
     }
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
-    Account updatedAccount = accounts.get(admin.id).get().getAccount();
+    Account updatedAccount = accounts.get(admin.id()).get().getAccount();
     assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
-    assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName);
+    assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName());
 
-    accountInfo = gApi.accounts().id(admin.id.get()).get();
+    accountInfo = gApi.accounts().id(admin.id().get()).get();
     assertThat(accountInfo.status).isEqualTo(Iterables.getLast(status));
-    assertThat(accountInfo.name).isEqualTo(admin.fullName);
+    assertThat(accountInfo.name).isEqualTo(admin.fullName());
   }
 
   @Test
   public void atomicReadMofifyWrite() throws Exception {
-    gApi.accounts().id(admin.id.get()).setStatus("A-1");
+    gApi.accounts().id(admin.id().get()).setStatus("A-1");
 
     AtomicInteger bgCounterA1 = new AtomicInteger(0);
     AtomicInteger bgCounterA2 = new AtomicInteger(0);
@@ -2371,7 +2384,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2385,19 +2398,19 @@
               try {
                 accountsUpdateProvider
                     .get()
-                    .update("Set Status", admin.id, u -> u.setStatus("A-2"));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+                    .update("Set Status", admin.id(), u -> u.setStatus("A-2"));
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             });
     assertThat(bgCounterA1.get()).isEqualTo(0);
     assertThat(bgCounterA2.get()).isEqualTo(0);
-    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("A-1");
+    assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("A-1");
 
     Optional<AccountState> updatedAccountState =
         update.update(
             "Set Status",
-            admin.id,
+            admin.id(),
             (a, u) -> {
               if ("A-1".equals(a.getAccount().getStatus())) {
                 bgCounterA1.getAndIncrement();
@@ -2415,8 +2428,8 @@
 
     assertThat(updatedAccountState).isPresent();
     assertThat(updatedAccountState.get().getAccount().getStatus()).isEqualTo("B-2");
-    assertThat(accounts.get(admin.id).get().getAccount().getStatus()).isEqualTo("B-2");
-    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("B-2");
+    assertThat(accounts.get(admin.id()).get().getAccount().getStatus()).isEqualTo("B-2");
+    assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("B-2");
   }
 
   @Test
@@ -2437,7 +2450,7 @@
         new AccountsUpdate(
             repoManager,
             gitReferenceUpdated,
-            null,
+            Optional.empty(),
             allUsers,
             externalIds,
             metaDataUpdateInternalFactory,
@@ -2455,17 +2468,14 @@
                         "Update External ID",
                         accountId,
                         u -> u.replaceExternalId(extIdA1, extIdA2));
-              } catch (IOException | ConfigInvalidException | OrmException e) {
+              } catch (IOException | ConfigInvalidException | StorageException e) {
                 // Ignore, the expected exception is asserted later
               }
             });
     assertThat(bgCounterA1.get()).isEqualTo(0);
     assertThat(bgCounterA2.get()).isEqualTo(0);
     assertThat(
-            gApi.accounts()
-                .id(accountId.get())
-                .getExternalIds()
-                .stream()
+            gApi.accounts().id(accountId.get()).getExternalIds().stream()
                 .map(i -> i.identity)
                 .collect(toSet()))
         .containsExactly(extIdA1.key().get());
@@ -2495,10 +2505,7 @@
     assertThat(updatedAccount.get().getExternalIds()).containsExactly(extIdB2);
     assertThat(accounts.get(accountId).get().getExternalIds()).containsExactly(extIdB2);
     assertThat(
-            gApi.accounts()
-                .id(accountId.get())
-                .getExternalIds()
-                .stream()
+            gApi.accounts().id(accountId.get()).getExternalIds().stream()
                 .map(i -> i.identity)
                 .collect(toSet()))
         .containsExactly(extIdB2.key().get());
@@ -2602,7 +2609,7 @@
               null);
 
       // Create 2 drafts each on both changes for user.
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
@@ -2611,12 +2618,12 @@
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
 
       // Create 1 draft on first change for admin.
-      requestScopeOperations.setApiUser(admin.getId());
+      requestScopeOperations.setApiUser(admin.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "admin draft");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
 
       // Delete user's draft comments; leave admin's alone.
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
 
@@ -2632,7 +2639,7 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
 
-      requestScopeOperations.setApiUser(admin.getId());
+      requestScopeOperations.setApiUser(admin.id());
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
     } finally {
       cleanUpDrafts();
@@ -2669,11 +2676,11 @@
   public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
     try {
       PushOneCommit.Result r = createChange();
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
-      requestScopeOperations.setApiUser(admin.getId());
+      requestScopeOperations.setApiUser(admin.id());
       try {
-        gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
+        gApi.accounts().id(user.id().get()).deleteDraftComments(new DeleteDraftCommentsInput());
         assert_().fail("expected AuthException");
       } catch (AuthException e) {
         assertThat(e).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
@@ -2690,7 +2697,7 @@
       PushOneCommit.Result r1 = createChange();
       PushOneCommit.Result r2 = createChange("refs/for/secret");
 
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -2712,18 +2719,6 @@
     }
   }
 
-  @Test
-  public void updateDisplayName() throws Exception {
-    String name = name("test");
-    gApi.accounts().create(name);
-    AuthRequest who = AuthRequest.forUser(name);
-    accountManager.authenticate(who);
-    assertThat(gApi.accounts().id(name).get().name).isEqualTo(name);
-    who.setDisplayName("Something Else");
-    accountManager.authenticate(who);
-    assertThat(gApi.accounts().id(name).get().name).isEqualTo("Something Else");
-  }
-
   private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
     DraftInput in = new DraftInput();
     in.path = path;
@@ -2734,14 +2729,10 @@
 
   private void cleanUpDrafts() throws Exception {
     for (TestAccount testAccount : accountCreator.getAll()) {
-      requestScopeOperations.setApiUser(testAccount.getId());
+      requestScopeOperations.setApiUser(testAccount.id());
       for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
         for (CommentInfo c :
-            gApi.changes()
-                .id(changeInfo.id)
-                .drafts()
-                .values()
-                .stream()
+            gApi.changes().id(changeInfo.id).drafts().values().stream()
                 .flatMap(List::stream)
                 .collect(toImmutableList())) {
           gApi.changes().id(changeInfo.id).revision(c.patchSet).draft(c.id).delete();
@@ -2751,18 +2742,15 @@
   }
 
   private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
-    return new Correspondence<GroupInfo, String>() {
-      @Override
-      public boolean compare(GroupInfo actualGroup, String expectedName) {
-        String groupName = actualGroup == null ? null : actualGroup.name;
-        return Objects.equals(groupName, expectedName);
-      }
-
-      @Override
-      public String toString() {
-        return "has name";
-      }
-    };
+    return Correspondence.from(
+        new BinaryPredicate<GroupInfo, String>() {
+          @Override
+          public boolean apply(GroupInfo actualGroup, String expectedName) {
+            String groupName = actualGroup == null ? null : actualGroup.name;
+            return Objects.equals(groupName, expectedName);
+          }
+        },
+        "has name");
   }
 
   private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
@@ -2829,9 +2817,7 @@
     Iterable<String> expectedFps =
         expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
     Iterable<String> actualFps =
-        externalIds
-            .byAccount(currAccountId, SCHEME_GPGKEY)
-            .stream()
+        externalIds.byAccount(currAccountId, SCHEME_GPGKEY).stream()
             .map(e -> e.key().id())
             .collect(toSet());
     assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
@@ -2850,7 +2836,9 @@
         .isEqualTo(Fingerprint.toString(expected.getPublicKey().getFingerprint()));
     List<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
     assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds);
-    assertThat(actual.key).named(id).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
+    String key = actual.key;
+    assertThat(key).named(id).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
+    assertThat(key).named(id).endsWith("-----END PGP PUBLIC KEY BLOCK-----\n");
     assertThat(actual.status).isEqualTo(GpgKeyInfo.Status.TRUSTED);
     assertThat(actual.problems).isEmpty();
   }
@@ -2861,12 +2849,12 @@
         .get()
         .update(
             "Add Email",
-            account.getId(),
+            account.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, account.getId(), email)));
+                    ExternalId.createWithEmail(name("test"), email, account.id(), email)));
     accountIndexedCounter.assertReindexOf(account);
-    requestScopeOperations.setApiUser(account.getId());
+    requestScopeOperations.setApiUser(account.id());
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
@@ -2886,9 +2874,9 @@
 
   private void assertUser(AccountInfo info, TestAccount account, @Nullable String expectedStatus)
       throws Exception {
-    assertThat(info.name).isEqualTo(account.fullName);
-    assertThat(info.email).isEqualTo(account.email);
-    assertThat(info.username).isEqualTo(account.username);
+    assertThat(info.name).isEqualTo(account.fullName());
+    assertThat(info.email).isEqualTo(account.email());
+    assertThat(info.username).isEqualTo(account.username());
     assertThat(info.status).isEqualTo(expectedStatus);
   }
 
@@ -2898,7 +2886,7 @@
 
   private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
     assertThat(accounts).hasSize(1);
-    assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
+    assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
   }
 
   private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
@@ -2947,7 +2935,7 @@
     }
 
     void assertReindexOf(TestAccount testAccount, int expectedCount) {
-      assertThat(getCount(testAccount.id)).isEqualTo(expectedCount);
+      assertThat(getCount(testAccount.id())).isEqualTo(expectedCount);
       assertThat(countsByAccount).hasSize(1);
       clear();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index ed5459d..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;
@@ -193,25 +193,28 @@
   }
 
   @Test
-  public void authenticateWhenUsernameExtIdAlreadyExists() throws Exception {
+  public void authenticateWithUsernameAndUpdateDisplayName() throws Exception {
     String username = "foo";
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
-    ExternalId.Key usernameExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_USERNAME, username);
-    assertNoSuchExternalIds(gerritExtIdKey, usernameExtIdKey);
-
-    // Create account with SCHEME_USERNAME external ID, but no SCHEME_GERRIT external ID.
+    String email = "foo@example.com";
     Account.Id accountId = new Account.Id(seq.nextAccountId());
+    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.setFullName("Foo").addExternalId(ExternalId.create(usernameExtIdKey, accountId)));
+        u ->
+            u.setFullName("Initial Name")
+                .setPreferredEmail(email)
+                .addExternalId(ExternalId.createWithEmail(gerritExtIdKey, accountId, email)));
 
     AuthRequest who = AuthRequest.forUser(username);
+    String newName = "Updated Name";
+    who.setDisplayName(newName);
     AuthResult authResult = accountManager.authenticate(who);
-
-    // Expect that the missing SCHEME_GERRIT external ID was created.
     assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
-    assertExternalIdsWithoutEmail(gerritExtIdKey, usernameExtIdKey);
+
+    Optional<AccountState> accountState = accounts.get(accountId);
+    assertThat(accountState).isPresent();
+    assertThat(accountState.get().getAccount().getFullName()).isEqualTo(newName);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 29d4aa0..a4a5745 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -125,7 +125,7 @@
   public void setUp() throws Exception {
     caAutoVerify = configureContributorAgreement(true);
     caNoAutoVerify = configureContributorAgreement(false);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
   }
 
   @Test
@@ -170,7 +170,7 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that the agreement was signed
     result = gApi.accounts().self().listAgreements();
@@ -226,12 +226,12 @@
     ChangeInfo change = gApi.changes().create(newChangeInput()).get();
 
     // Approve and submit it
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Revert is not allowed when CLA is required but not signed
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     exception.expect(AuthException.class);
     exception.expectMessage("Contributor Agreement");
@@ -250,12 +250,12 @@
     ChangeInfo change = gApi.changes().create(newChangeInput()).get();
 
     // Approve and submit it
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Revert in excluded project is allowed even when CLA is required but not signed
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     gApi.changes().id(change.changeId).revert();
   }
@@ -265,7 +265,7 @@
     assume().that(isContributorAgreementsEnabled()).isTrue();
 
     // Create a new branch
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     BranchInfo dest =
         gApi.projects()
             .name(project.get())
@@ -282,7 +282,7 @@
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Cherry-pick is not allowed when CLA is required but not signed
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     CherryPickInput in = new CherryPickInput();
     in.destination = dest.ref;
@@ -313,7 +313,7 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // Create a change succeeds after signing the agreement
     gApi.changes().create(newChangeInput());
@@ -360,7 +360,7 @@
   public void publishEditRestWithoutCLA() throws Exception {
     String filename = "foo";
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "subject1", filename, "contentold");
+        pushFactory.create(admin.newIdent(), testRepo, "subject1", filename, "contentold");
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
     String changeId = result.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index ba340eb..d7e765b 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -29,7 +29,7 @@
   @Test
   public void getDiffPreferences() throws Exception {
     DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertPrefs(o, d);
   }
 
@@ -64,13 +64,13 @@
     i.matchBrackets ^= true;
     i.lineWrapping ^= true;
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertPrefs(o, i);
 
     // Partially fill input record
     i = new DiffPreferencesInfo();
     i.tabSize = 42;
-    DiffPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertPrefs(a, o, "tabSize");
     assertThat(a.tabSize).isEqualTo(42);
   }
@@ -87,7 +87,7 @@
     update.fontSize = newFontSize;
     gApi.config().server().setDefaultDiffPreferences(update);
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
 
     // assert configured defaults
     assertThat(o.lineLength).isEqualTo(newLineLength);
@@ -106,29 +106,29 @@
     update.lineLength = configuredDefaultLineLength;
     gApi.config().server().setDefaultDiffPreferences(update);
 
-    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(o.lineLength).isEqualTo(configuredDefaultLineLength);
     assertPrefs(o, d, "lineLength");
 
     int newLineLength = configuredDefaultLineLength + 10;
     DiffPreferencesInfo i = new DiffPreferencesInfo();
     i.lineLength = newLineLength;
-    DiffPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertThat(a.lineLength).isEqualTo(newLineLength);
     assertPrefs(a, d, "lineLength");
 
-    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(a.lineLength).isEqualTo(newLineLength);
     assertPrefs(a, d, "lineLength");
 
     // overwrite the configured default with original hard-coded default
     i = new DiffPreferencesInfo();
     i.lineLength = d.lineLength;
-    a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
     assertThat(a.lineLength).isEqualTo(d.lineLength);
     assertPrefs(a, d, "lineLength");
 
-    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
     assertThat(a.lineLength).isEqualTo(d.lineLength);
     assertPrefs(a, d, "lineLength");
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
index c1d9bcb..00d1733 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
@@ -27,7 +27,7 @@
 public class EditPreferencesIT extends AbstractDaemonTest {
   @Test
   public void getSetEditPreferences() throws Exception {
-    EditPreferencesInfo out = gApi.accounts().id(admin.getId().toString()).getEditPreferences();
+    EditPreferencesInfo out = gApi.accounts().id(admin.id().toString()).getEditPreferences();
 
     assertThat(out.lineLength).isEqualTo(100);
     assertThat(out.indentUnit).isEqualTo(2);
@@ -64,7 +64,7 @@
     out.theme = Theme.TWILIGHT;
     out.keyMapType = KeyMapType.EMACS;
 
-    EditPreferencesInfo info = gApi.accounts().id(admin.getId().toString()).setEditPreferences(out);
+    EditPreferencesInfo info = gApi.accounts().id(admin.id().toString()).setEditPreferences(out);
 
     assertEditPreferences(info, out);
 
@@ -72,7 +72,7 @@
     EditPreferencesInfo in = new EditPreferencesInfo();
     in.tabSize = 42;
 
-    info = gApi.accounts().id(admin.getId().toString()).setEditPreferences(in);
+    info = gApi.accounts().id(admin.id().toString()).setEditPreferences(in);
 
     out.tabSize = in.tabSize;
     assertEditPreferences(info, out);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 24040a4..70e37ef 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -56,7 +56,7 @@
 
   @Test
   public void getAndSetPreferences() throws Exception {
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.id.toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
     assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
     assertThat(o.my)
         .containsExactly(
@@ -96,7 +96,7 @@
     i.urlAliases = new HashMap<>();
     i.urlAliases.put("foo", "bar");
 
-    o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
     assertPrefs(o, i, "my");
     assertThat(o.my).containsExactlyElementsIn(i.my);
     assertThat(o.changeTable).containsExactlyElementsIn(i.changeTable);
@@ -110,7 +110,7 @@
     update.changesPerPage = newChangesPerPage;
     gApi.config().server().setDefaultPreferences(update);
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
 
     // assert configured defaults
     assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
@@ -127,29 +127,29 @@
     update.changesPerPage = configuredChangesPerPage;
     gApi.config().server().setDefaultPreferences(update);
 
-    GeneralPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(o.changesPerPage).isEqualTo(configuredChangesPerPage);
     assertPrefs(o, d, "my", "changeTable", "changesPerPage");
 
     int newChangesPerPage = configuredChangesPerPage * 2;
     GeneralPreferencesInfo i = new GeneralPreferencesInfo();
     i.changesPerPage = newChangesPerPage;
-    GeneralPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    GeneralPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
     assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
-    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
     // overwrite the configured default with original hard-coded default
     i = new GeneralPreferencesInfo();
     i.changesPerPage = d.changesPerPage;
-    a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
     assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
 
-    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    a = gApi.accounts().id(admin.id().toString()).getPreferences();
     assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
     assertPrefs(a, d, "my", "changeTable", "changesPerPage");
   }
@@ -162,7 +162,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("name for menu item is required");
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -173,7 +173,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("URL for menu item is required");
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -182,7 +182,7 @@
     i.my = new ArrayList<>();
     i.my.add(new MenuItem(" name\t", " url\t", " _blank\t", " id\t"));
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
     assertThat(o.my).containsExactly(new MenuItem("name", "url", "_blank", "id"));
   }
 
@@ -193,7 +193,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("Unsupported download scheme: " + i.downloadScheme);
-    gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+    gApi.accounts().id(user42.id().toString()).setPreferences(i);
   }
 
   @Test
@@ -206,10 +206,10 @@
       GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
       i.downloadScheme = schemeName;
 
-      GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).setPreferences(i);
+      GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
       assertThat(o.downloadScheme).isEqualTo(schemeName);
 
-      o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+      o = gApi.accounts().id(user42.id().toString()).getPreferences();
       assertThat(o.downloadScheme).isEqualTo(schemeName);
     } finally {
       registrationHandle.remove();
@@ -222,7 +222,7 @@
     // becomes unsupported.
     setDownloadScheme();
 
-    GeneralPreferencesInfo o = gApi.accounts().id(user42.getId().toString()).getPreferences();
+    GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
     assertThat(o.downloadScheme).isNull();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index e655053..925c66a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.AbandonUtil;
+import com.google.gerrit.server.config.ChangeCleanupConfig;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
@@ -46,6 +47,7 @@
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ChangeCleanupConfig cleanupConfig;
 
   @Test
   public void abandon() throws Exception {
@@ -125,11 +127,36 @@
   }
 
   @Test
+  public void changeCleanupConfigDefaultAbandonMessage() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage())
+        .startsWith(
+            "Auto-Abandoned due to inactivity, see "
+                + canonicalWebUrl.get()
+                + "Documentation/user-change-cleanup.html#auto-abandon");
+  }
+
+  @Test
+  @GerritConfig(name = "changeCleanup.abandonMessage", value = "XX ${URL} XX")
+  public void changeCleanupConfigCustomAbandonMessageWithUrlReplacement() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage())
+        .isEqualTo(
+            "XX "
+                + canonicalWebUrl.get()
+                + "Documentation/user-change-cleanup.html#auto-abandon XX");
+  }
+
+  @Test
+  @GerritConfig(name = "changeCleanup.abandonMessage", value = "XX YYY XX")
+  public void changeCleanupConfigCustomAbandonMessageWithoutUrlReplacement() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage()).isEqualTo("XX YYY XX");
+  }
+
+  @Test
   public void abandonNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("abandon not permitted");
     gApi.changes().id(changeId).abandon();
@@ -141,7 +168,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     gApi.changes().id(changeId).restore();
@@ -172,7 +199,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes().id(changeId).abandon();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     exception.expect(AuthException.class);
     exception.expectMessage("restore not permitted");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 81acb3f..ebb3978 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -57,8 +57,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.truth.ThrowableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.ChangeIndexedCounter;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GitUtil;
@@ -70,13 +70,16 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
+import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
@@ -86,6 +89,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
@@ -127,6 +131,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -145,6 +150,7 @@
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -152,6 +158,7 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -162,6 +169,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
@@ -234,7 +242,7 @@
     assertThat(c.created).isEqualTo(c.updated);
     assertThat(c._number).isEqualTo(r.getChange().getId().get());
 
-    assertThat(c.owner._accountId).isEqualTo(admin.getId().get());
+    assertThat(c.owner._accountId).isEqualTo(admin.id().get());
     assertThat(c.owner.name).isNull();
     assertThat(c.owner.email).isNull();
     assertThat(c.owner.username).isNull();
@@ -254,158 +262,11 @@
   }
 
   @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 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();
-    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(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 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();
-    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);
-
-    requestScopeOperations.setApiUser(user.getId());
-    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
@@ -413,32 +274,32 @@
     PushOneCommit.Result rwip = createChange();
     String changeId = rwip.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setWorkInProgress();
   }
 
   @Test
   public void setWorkInProgressAllowedAsAdmin() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
 
   @Test
   public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
@@ -459,34 +320,34 @@
     String changeId = rready.getChangeId();
     gApi.changes().id(changeId).setWorkInProgress();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setReadyForReview();
   }
 
   @Test
   public void setReadyForReviewAllowedAsAdmin() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
@@ -559,7 +420,7 @@
         ais -> ais.stream().map(ai -> ai.email).collect(toSet());
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
         .containsExactly(
-            admin.email, email1, email2, "byemail1@example.com", "byemail2@example.com");
+            admin.email(), email1, email2, "byemail1@example.com", "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email3, email4, "byemail3@example.com", "byemail4@example.com");
     assertThat(info.pendingReviewers.get(REMOVED)).isNull();
@@ -571,7 +432,7 @@
     gApi.changes().id(changeId).reviewer("byemail3@example.com").remove();
     info = gApi.changes().id(changeId).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
-        .containsExactly(admin.email, email2, "byemail2@example.com");
+        .containsExactly(admin.email(), email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -582,7 +443,7 @@
     gApi.changes().id(changeId).revision("current").review(in);
     info = gApi.changes().id(changeId).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
-        .containsExactly(admin.email, email1, email2, "byemail2@example.com");
+        .containsExactly(admin.email(), email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
@@ -593,7 +454,7 @@
     info = gApi.changes().id(changeId).get();
     assertThat(info.pendingReviewers).isEmpty();
     assertThat(toEmails.apply(info.reviewers.get(REVIEWER)))
-        .containsExactly(admin.email, email1, email2, "byemail2@example.com");
+        .containsExactly(admin.email(), email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.reviewers.get(CC)))
         .containsExactly(email4, "byemail4@example.com");
     assertThat(info.reviewers.get(REMOVED)).isNull();
@@ -638,6 +499,37 @@
   }
 
   @Test
+  public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String refactor = "Needs some refactoring";
+    String ptal = "PTAL";
+
+    grant(
+        project,
+        "refs/heads/master",
+        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
+        false,
+        REGISTERED_USERS);
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setWorkInProgress(refactor);
+
+    ChangeInfo info = gApi.changes().id(changeId).get();
+
+    assertThat(info.workInProgress).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).contains(refactor);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
+
+    gApi.changes().id(changeId).setReadyForReview(ptal);
+
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.workInProgress).isNull();
+    assertThat(Iterables.getLast(info.messages).message).contains(ptal);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
+  }
+
+  @Test
   public void reviewAndStartReview() throws Exception {
     PushOneCommit.Result r = createWorkInProgressChange();
     r.assertOkStatus();
@@ -670,14 +562,17 @@
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
     ReviewInput in =
-        ReviewInput.approve().reviewer(user.email).label("Code-Review", 1).setWorkInProgress(true);
+        ReviewInput.approve()
+            .reviewer(user.email())
+            .label("Code-Review", 1)
+            .setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).revision("current").review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
     assertThat(info.workInProgress).isTrue();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(admin.id.get(), user.id.get());
-    assertThat(info.labels.get("Code-Review").recommended._accountId).isEqualTo(admin.id.get());
+        .containsExactly(admin.id().get(), user.id().get());
+    assertThat(info.labels.get("Code-Review").recommended._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
@@ -693,12 +588,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWorkInProgressChangeOwner() throws Exception {
-    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -708,12 +603,12 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void reviewWithWithWorkInProgressAdmin() throws Exception {
-    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -724,19 +619,35 @@
   public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
   @Test
+  public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
+    grant(
+        project,
+        "refs/heads/master",
+        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
+        false,
+        REGISTERED_USERS);
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(r.getChangeId()).current().review(in);
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    assertThat(info.workInProgress).isTrue();
+  }
+
+  @Test
   public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setReady(true);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
@@ -752,7 +663,7 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -793,7 +704,7 @@
   @Test
   public void revertNotifications() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
@@ -809,7 +720,7 @@
   @Test
   public void suppressRevertNotifications() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
@@ -826,17 +737,17 @@
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = ReviewInput.approve();
-    in.reviewer(user.email);
-    in.reviewer(accountCreator.user2().email, ReviewerState.CC, true);
+    in.reviewer(user.email());
+    in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
     // Add user as reviewer that will create the revert
-    in.reviewer(accountCreator.admin2().email);
+    in.reviewer(accountCreator.admin2().email());
 
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
     // expect both the original reviewers and CCs to be preserved
     // original owner should be added as reviewer, user requesting the revert (new owner) removed
-    requestScopeOperations.setApiUser(accountCreator.admin2().getId());
+    requestScopeOperations.setApiUser(accountCreator.admin2().id());
     Map<ReviewerState, Collection<AccountInfo>> result =
         gApi.changes().id(r.getChangeId()).revert().get().reviewers;
     assertThat(result).containsKey(ReviewerState.REVIEWER);
@@ -846,8 +757,8 @@
     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());
+    assertThat(ccs).containsExactly(accountCreator.user2().id().get());
+    assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
   }
 
   @Test
@@ -902,8 +813,8 @@
     // ...and the committer and description should be correct
     ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
     GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
-    assertThat(committer.name).isEqualTo(admin.fullName);
-    assertThat(committer.email).isEqualTo(admin.email);
+    assertThat(committer.name).isEqualTo(admin.fullName());
+    assertThat(committer.email).isEqualTo(admin.email());
     String description = info.revisions.get(info.currentRevision).description;
     assertThat(description).isEqualTo("Rebase");
 
@@ -1018,7 +929,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1040,7 +951,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).rebase();
   }
 
@@ -1061,7 +972,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1097,10 +1008,10 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteNewChangeAsNormalUser() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
@@ -1159,7 +1070,7 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
-      requestScopeOperations.setApiUser(owner.getId());
+      requestScopeOperations.setApiUser(owner.id());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
       in.branch = "refs/heads/master";
@@ -1169,16 +1080,16 @@
       int id = changeInfo._number;
       String commit = changeInfo.currentRevision;
 
-      assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id.get());
+      assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id().get());
 
-      requestScopeOperations.setApiUser(deleteAs.getId());
+      requestScopeOperations.setApiUser(deleteAs.id());
       gApi.changes().id(changeId).delete();
 
       assertThat(query(changeId)).isEmpty();
 
       String ref = new Change.Id(id).toRefPrefix() + "1";
       eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
-      eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email);
+      eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
     } finally {
       removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
       removePermission(project, "refs/*", Permission.DELETE_CHANGES);
@@ -1198,7 +1109,7 @@
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
 
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       exception.expect(AuthException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1222,10 +1133,10 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteAbandonedChangeAsNormalUser() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
 
     exception.expect(AuthException.class);
@@ -1237,7 +1148,7 @@
   @TestProjectInput(cloneAs = "user")
   public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
     PushOneCommit.Result changeResult =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
     gApi.changes().id(changeId).abandon();
@@ -1266,12 +1177,12 @@
 
     try {
       PushOneCommit.Result changeResult =
-          pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+          pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
       String changeId = changeResult.getChangeId();
 
       merge(changeResult);
 
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       exception.expect(MethodNotAllowedException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1313,6 +1224,34 @@
   }
 
   @Test
+  public void deleteChangeRemovesDraftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    DraftInput dri = new DraftInput();
+    dri.message = "hello";
+    dri.path = "a.txt";
+    dri.line = 1;
+
+    gApi.changes().id(r.getChangeId()).current().createDraft(dri);
+    Change.Id num = r.getChange().getId();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
+          .isNotEmpty();
+    }
+
+    requestScopeOperations.setApiUser(admin.id());
+
+    gApi.changes().id(r.getChangeId()).delete();
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
+          .isEmpty();
+    }
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
@@ -1328,7 +1267,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -1484,31 +1423,31 @@
   @Test
   public void pushCommitOfOtherUser() throws Exception {
     // admin pushes commit of user
-    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
     CommitInfo commit = change.revisions.get(change.currentRevision).commit;
-    assertThat(commit.author.email).isEqualTo(user.email);
-    assertThat(commit.committer.email).isEqualTo(user.email);
+    assertThat(commit.author.email).isEqualTo(user.email());
+    assertThat(commit.committer.email).isEqualTo(user.email());
 
     // check that the author/committer was added as reviewer
     Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
     assertThat(change.reviewers.get(CC)).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
     assertThat(m.from().getName()).isEqualTo("Administrator (Code Review)");
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("I'd like you to do a code review");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
   }
 
   @Test
@@ -1523,18 +1462,18 @@
 
     // admin pushes commit of user
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(user.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
     CommitInfo commit = change.revisions.get(change.currentRevision).commit;
-    assertThat(commit.author.email).isEqualTo(user.email);
-    assertThat(commit.committer.email).isEqualTo(user.email);
+    assertThat(commit.author.email).isEqualTo(user.email());
+    assertThat(commit.committer.email).isEqualTo(user.email());
 
     // check the user cannot see the change
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1554,13 +1493,13 @@
     // admin pushes commit that references 'user' in a footer
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
                 + "\n\n"
                 + FooterConstants.REVIEWED_BY.getName()
                 + ": "
-                + user.getIdent().toExternalString(),
+                + user.newIdent().toExternalString(),
             PushOneCommit.FILE_NAME,
             PushOneCommit.FILE_CONTENT);
     PushOneCommit.Result result = push.to("refs/for/master");
@@ -1571,17 +1510,17 @@
     Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
     assertThat(change.reviewers.get(CC)).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
   }
 
   @Test
@@ -1598,20 +1537,20 @@
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             repo,
             PushOneCommit.SUBJECT
                 + "\n\n"
                 + FooterConstants.REVIEWED_BY.getName()
                 + ": "
-                + user.getIdent().toExternalString(),
+                + user.newIdent().toExternalString(),
             PushOneCommit.FILE_NAME,
             PushOneCommit.FILE_CONTENT);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     // check that 'user' cannot see the change
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1620,7 +1559,7 @@
     }
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
     assertThat(change.reviewers.get(REVIEWER)).isNull();
     assertThat(change.reviewers.get(CC)).isNull();
@@ -1639,12 +1578,12 @@
 
     // create change
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     // check the user cannot see the change
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1653,12 +1592,12 @@
     }
 
     // try to add user as reviewer
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
 
-    assertThat(r.input).isEqualTo(user.email);
+    assertThat(r.input).isEqualTo(user.email());
     assertThat(r.error).contains("does not have permission to see this change");
     assertThat(r.reviewers).isNull();
   }
@@ -1668,18 +1607,48 @@
     PushOneCommit.Result result = createChange();
 
     String username = name("new-user");
-    accountOperations.newAccount().username(username).inactive().create();
+    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 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;
@@ -1711,17 +1680,17 @@
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email);
+    assertMailReplyTo(m, admin.email());
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     // When NoteDb is enabled adding a reviewer records that user as reviewer
@@ -1731,7 +1700,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -1740,15 +1709,38 @@
 
     // Change status of reviewer and ensure ETag is updated.
     oldETag = rsrc.getETag();
-    accountOperations.account(user.id).forUpdate().status("new status").update();
+    accountOperations.account(user.id()).forUpdate().status("new status").update();
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
   }
 
   @Test
+  public void listReviewers() throws Exception {
+    PushOneCommit.Result r = createChange();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email();
+    gApi.changes().id(r.getChangeId()).addReviewer(in);
+    assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1);
+
+    String username1 = name("user1");
+    String email1 = username1 + "@example.com";
+    accountOperations
+        .newAccount()
+        .username(username1)
+        .preferredEmail(email1)
+        .fullname("User 1")
+        .create();
+    in.reviewer = email1;
+    in.state = ReviewerState.CC;
+    gApi.changes().id(r.getChangeId()).addReviewer(in);
+    assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
+        .containsExactly(user.username(), username1);
+  }
+
+  @Test
   public void notificationsForAddedWorkInProgressReviewers() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     ReviewInput batchIn = new ReviewInput();
     batchIn.reviewers = ImmutableList.of(in);
 
@@ -1800,7 +1792,7 @@
     String testGroup = groupOperations.newGroup().name("ab").create().get();
     GroupApi groupApi = gApi.groups().id(testGroup);
     groupApi.description("test group");
-    groupApi.addMembers(user.fullName);
+    groupApi.addMembers(user.fullName());
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = "abc";
@@ -1904,8 +1896,8 @@
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
-    requestScopeOperations.setApiUser(user.getId());
+    in.reviewer = user.email();
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     // There should be no email notification when adding self
@@ -1919,7 +1911,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -1935,15 +1927,15 @@
   @Test
   public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
     com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
-    assertThat(accountWithoutUsername.username).isNull();
+    assertThat(accountWithoutUsername.username()).isNull();
     testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
   }
 
   private void testImplicitlyCcOnNonVotingReviewPgStyle(
       com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(testAccount.getId());
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
+    requestScopeOperations.setApiUser(testAccount.id());
+    assertThat(getReviewerState(r.getChangeId(), testAccount.id())).isEmpty();
 
     // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
     ReviewInput in = new ReviewInput();
@@ -1953,13 +1945,13 @@
     in.reviewers = ImmutableList.of();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
 
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id)).hasValue(CC);
+    assertThat(getReviewerState(r.getChangeId(), testAccount.id())).hasValue(CC);
   }
 
   @Test
   public void implicitlyAddReviewerOnVotingReview() throws Exception {
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -1967,23 +1959,23 @@
 
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(user.id.get());
+        .containsExactly(user.id().get());
 
     // Further test: remove the vote, then comment again. The user should be
     // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb.
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).remove();
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.values()).isEmpty();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
         .review(new ReviewInput().message("hi"));
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.get(CC).stream().map(ai -> ai._accountId).collect(toList()))
-        .containsExactly(user.id.get());
+        .containsExactly(user.id().get());
   }
 
   @Test
@@ -1995,19 +1987,19 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.id().get());
     assertThat(c.reviewers).doesNotContainKey(CC);
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     c = gApi.changes().id(r.getChangeId()).get();
     reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(2);
     Iterator<AccountInfo> reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
-    assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
     assertThat(c.reviewers).doesNotContainKey(CC);
   }
 
@@ -2017,7 +2009,7 @@
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
 
-    accountOperations.account(admin.id).forUpdate().status("new status").update();
+    accountOperations.account(admin.id()).forUpdate().status("new status").update();
     rsrc = parseResource(r);
     assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
   }
@@ -2027,7 +2019,7 @@
     String changeId = createChange().getChangeId();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
     sender.clear();
 
@@ -2042,7 +2034,7 @@
 
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
@@ -2073,15 +2065,15 @@
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes();
 
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2));
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike());
 
-    m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
+    m = gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
 
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) -1));
@@ -2107,32 +2099,32 @@
 
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
-    gApi.changes().id(changeId).addReviewer(user.getId().toString());
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
 
     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());
+    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
 
     sender.clear();
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
     assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
 
     assertThat(sender.getMessages()).hasSize(1);
     Message message = sender.getMessages().get(0);
-    assertThat(message.body()).contains("Removed reviewer " + user.fullName + ".");
+    assertThat(message.body()).contains("Removed reviewer " + user.fullName() + ".");
     assertThat(message.body()).doesNotContain("with the following votes");
 
     // Make sure the reviewer can still be added again.
-    gApi.changes().id(changeId).addReviewer(user.getId().toString());
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
     c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
-    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.getId());
+    assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
 
     // Remove again, and then try to remove once more to verify 404 is
     // returned.
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
     exception.expect(ResourceNotFoundException.class);
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove();
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
   }
 
   @Test
@@ -2150,30 +2142,30 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend());
 
     Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
 
     assertThat(reviewers).hasSize(2);
     Iterator<AccountInfo> reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
-    assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
 
     sender.clear();
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     DeleteReviewerInput input = new DeleteReviewerInput();
     if (!notify) {
       input.notify = NotifyHandling.NONE;
     }
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).remove(input);
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove(input);
 
     if (notify) {
       assertThat(sender.getMessages()).hasSize(1);
       Message message = sender.getMessages().get(0);
       assertThat(message.body())
-          .contains("Removed reviewer " + user.fullName + " with the following votes");
-      assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName);
+          .contains("Removed reviewer " + user.fullName() + " with the following votes");
+      assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName());
     } else {
       assertThat(sender.getMessages()).isEmpty();
     }
@@ -2181,9 +2173,9 @@
     reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
     assertThat(reviewers).hasSize(1);
     reviewerIt = reviewers.iterator();
-    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
 
-    eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+    eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
   }
 
   @Test
@@ -2192,10 +2184,10 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
   }
 
   @Test
@@ -2203,14 +2195,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeId);
     gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
@@ -2221,15 +2213,15 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).abandon();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
-    eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+    eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
   }
 
   @Test
@@ -2237,17 +2229,17 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(changeId);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeId);
     gApi.changes().id(changeId).abandon();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove();
   }
 
   @Test
@@ -2255,23 +2247,23 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote("Code-Review");
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote("Code-Review");
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message msg = messages.get(0);
-    assertThat(msg.rcpt()).containsExactly(user.emailAddress);
-    assertThat(msg.body()).contains(admin.fullName + " has removed a vote on this change.\n");
+    assertThat(msg.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(msg.body()).contains(admin.fullName() + " has removed a vote on this change.\n");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
+        .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
 
     // Dummy 0 approval on the change to block vote copying to this patch set.
     assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 0));
@@ -2279,10 +2271,10 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   @Test
@@ -2290,15 +2282,15 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
     in.notify = NotifyHandling.NONE;
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertThat(sender.getMessages()).isEmpty();
   }
 
@@ -2319,33 +2311,33 @@
         .preferredEmail(email)
         .fullname("User2")
         .create();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyTo(email, "User2");
 
     // notify unrelated account as CC
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyCc(email, "User2");
 
     // notify unrelated account as BCC
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
-    gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
+    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
     assertNotifyBcc(email, "User2");
   }
 
@@ -2354,10 +2346,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("delete vote not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).deleteVote("Code-Review");
+    gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).deleteVote("Code-Review");
   }
 
   @Test
@@ -2385,31 +2377,31 @@
     // Reviewers should only be "admin"
     ChangeInfo c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id()));
     assertThat(c.reviewers.get(CC)).isNull();
 
     // Add the user as reviewer
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
     c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
 
     // Approve the change as user, then remove the approval
     // (only to confirm that the user does have Code-Review+2 permission)
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore());
 
     // Submit the change
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).revision(commit).submit();
 
     // User should still be on the change
     c = gApi.changes().id(changeId).get();
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   @Test
@@ -2420,7 +2412,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.");
   }
@@ -2467,6 +2459,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();
@@ -2509,7 +2514,7 @@
     RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
     assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
     assertThat(rev.created).isNotNull();
-    assertThat(rev.uploader._accountId).isEqualTo(admin.getId().get());
+    assertThat(rev.uploader._accountId).isEqualTo(admin.id().get());
     assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
     assertThat(rev.actions).isNotEmpty();
   }
@@ -2520,18 +2525,59 @@
     assertThat(
             Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
         .isEqualTo(r.getChangeId());
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
   }
 
+  private static class OperatorModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeOperatorFactory.class)
+          .annotatedWith(Exports.named("mytopic"))
+          .toInstance((cqb, value) -> new MyTopicPredicate(value));
+    }
+
+    private static class MyTopicPredicate extends PostFilterPredicate<ChangeData> {
+      MyTopicPredicate(String value) {
+        super("mytopic", value);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) {
+        return Objects.equals(cd.change().getTopic(), value);
+      }
+
+      @Override
+      public int getCost() {
+        return 2;
+      }
+    }
+  }
+
+  @Test
+  public void queryChangesPluginOperator() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String query = "mytopic_myplugin:foo";
+    String expectedMessage = "Unsupported operator mytopic_myplugin:foo";
+    assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
+
+    try (AutoCloseable ignored = installPlugin("myplugin", OperatorModule.class)) {
+      assertThat(query(query)).isEmpty();
+      gApi.changes().id(r.getChangeId()).topic("foo");
+      assertThat(query(query).stream().map(i -> i.changeId)).containsExactly(r.getChangeId());
+    }
+
+    assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
+  }
+
   @Test
   public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull();
 
     revision(r).review(ReviewInput.recommend());
@@ -2552,7 +2598,7 @@
   public void editTopicWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("edit topic name not permitted");
     gApi.changes().id(r.getChangeId()).topic("mytopic");
@@ -2563,7 +2609,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);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
   }
@@ -2607,7 +2653,7 @@
   public void submitNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("submit not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
@@ -2618,7 +2664,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);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
     assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
   }
@@ -2654,7 +2700,7 @@
     r1.assertOkStatus();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
     r2.assertOkStatus();
 
@@ -2682,7 +2728,12 @@
     List<String> expectedFooters =
         Arrays.asList(
             "Change-Id: " + r2.getChangeId(),
-            "Reviewed-on: " + canonicalWebUrl.get() + "c/" + r2.getChange().getId(),
+            "Reviewed-on: "
+                + canonicalWebUrl.get()
+                + "c/"
+                + project.get()
+                + "/+/"
+                + r2.getChange().getId(),
             "Reviewed-by: Administrator <admin@example.com>",
             "Custom2: Administrator <admin@example.com>",
             "Tested-by: Administrator <admin@example.com>");
@@ -2717,14 +2768,19 @@
     List<String> expectedFooters =
         Arrays.asList(
             "Change-Id: " + change.getChangeId(),
-            "Reviewed-on: " + canonicalWebUrl.get() + "c/" + change.getChange().getId(),
+            "Reviewed-on: "
+                + canonicalWebUrl.get()
+                + "c/"
+                + project.get()
+                + "/+/"
+                + change.getChange().getId(),
             "Custom: refs/heads/master");
     assertThat(footers).containsExactlyElementsIn(expectedFooters);
   }
 
   @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     PushOneCommit.Result r1 = createChange();
     gApi.changes()
         .id(r1.getChangeId())
@@ -2734,9 +2790,8 @@
 
     createChange();
 
-    requestScopeOperations.setApiUser(user.getId());
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    requestScopeOperations.setApiUser(user.id());
+    try (AutoCloseable ignored = disableNoteDb()) {
       assertThat(
               gApi.changes()
                   .query()
@@ -2747,8 +2802,6 @@
                   .withOption(REVIEWED)
                   .get())
           .hasSize(2);
-    } finally {
-      enableDb(ctx);
     }
   }
 
@@ -2756,12 +2809,12 @@
   public void votable() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isEqualTo(0);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -2773,7 +2826,7 @@
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isNull();
   }
 
@@ -2822,7 +2875,7 @@
   public void noteDbCommitsOnPatchSetCreation() throws Exception {
     PushOneCommit.Result r = createChange();
     pushFactory
-        .create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
+        .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
         .to("refs/for/master")
         .assertOkStatus();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
@@ -2833,7 +2886,7 @@
 
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.updated, serverIdent.get());
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.updated, serverIdent.get());
       assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitPatchSetCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -2841,7 +2894,8 @@
 
       RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
       assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
-      expectedAuthor = changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+      expectedAuthor =
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.created, serverIdent.get());
       assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitChangeCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
@@ -2858,7 +2912,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.");
   }
@@ -2888,7 +2942,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2908,7 +2962,7 @@
     TestRepository<?> userTestRepo = cloneProject(project, user);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2932,7 +2986,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as admin
-    PushOneCommit push = pushFactory.create(admin.getIdent(), adminTestRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -2964,7 +3018,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3000,7 +3054,7 @@
     createBranch("dev");
     PushOneCommit.Result changeA =
         pushFactory
-            .create(user.getIdent(), testRepo, "change A", "A.txt", "A content")
+            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
             .to("refs/heads/dev");
     changeA.assertOkStatus();
     MergeInput mergeInput = new MergeInput();
@@ -3036,7 +3090,7 @@
     gApi.changes().id(baseChange).setPrivate(true, "set private");
 
     // Create the destination change on 'master' branch.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     testRepo.reset(initialHead);
     String changeId = createChange().getChangeId();
 
@@ -3174,7 +3228,7 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Ignore Verified",
             "rules.pl",
@@ -3220,7 +3274,7 @@
     testRepo.reset("config");
     PushOneCommit push2 =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Configure Non-Author-Code-Review",
             "rules.pl",
@@ -3257,10 +3311,10 @@
 
     PushOneCommit.Result r = createChange();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
     ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
@@ -3274,7 +3328,7 @@
   public void checkLabelsForAutoClosedChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to("refs/heads/master");
     result.assertOkStatus();
 
@@ -3293,13 +3347,13 @@
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
 
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
 
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNotNull();
     // default values
     assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
@@ -3320,7 +3374,7 @@
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNotNull();
     assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue);
     assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue);
@@ -3336,13 +3390,13 @@
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
 
-    gApi.changes().id(triplet).addReviewer(user.username);
+    gApi.changes().id(triplet).addReviewer(user.username());
 
     ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.permittedVotingRange).isNull();
   }
 
@@ -3354,7 +3408,8 @@
     ReviewInput input = ReviewInput.approve().label("Code-Style", 1);
     gApi.changes().id(changeId).current().review(input);
 
-    Map<String, Short> votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes();
+    Map<String, Short> votes =
+        gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
     assertThat(votes.keySet()).containsExactly("Code-Review");
     assertThat(votes.values()).containsExactly((short) 2);
   }
@@ -3367,7 +3422,8 @@
     ReviewInput input = new ReviewInput().label("Code-Review", 3);
     gApi.changes().id(changeId).current().review(input);
 
-    Map<String, Short> votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes();
+    Map<String, Short> votes =
+        gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
     assertThat(votes).isEmpty();
   }
 
@@ -3408,10 +3464,10 @@
 
     String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     PushOneCommit.Result result2 =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
 
     addComment(result1, "comment 1", true, false, null);
     addComment(result2, "comment 2", true, true, null);
@@ -3429,7 +3485,7 @@
     addPureRevertSubmitRule();
 
     // Create a change that is not a revert of another change
-    PushOneCommit.Result r1 = pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     approve(r1.getChangeId());
 
     exception.expect(ResourceConflictException.class);
@@ -3440,7 +3496,7 @@
 
   @Test
   public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
-    PushOneCommit.Result r1 = pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3459,7 +3515,7 @@
   @Test
   public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
     // Create a change that we can later revert
-    PushOneCommit.Result r1 = pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     merge(r1);
 
     addPureRevertSubmitRule();
@@ -3480,9 +3536,9 @@
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
 
     for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
-      requestScopeOperations.setApiUser(acc.getId());
+      requestScopeOperations.setApiUser(acc.id());
       String newMessage =
-          "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
+          "modified commit by " + acc.username() + "\n\nChange-Id: " + r.getChangeId() + "\n";
       gApi.changes().id(r.getChangeId()).setMessage(newMessage);
       RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
       assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
@@ -3499,7 +3555,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.
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(r.getChangeId()).setWorkInProgress();
     String newMessage = "modified commit in WIP change\n\nChange-Id: " + r.getChangeId() + "\n";
     gApi.changes().id(r.getChangeId()).setMessage(newMessage);
@@ -3537,6 +3593,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();
@@ -3557,7 +3625,7 @@
     // Block default permission
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
     // Create change as user
-    PushOneCommit push = pushFactory.create(user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     // Try to change the commit message
@@ -3587,7 +3655,7 @@
     String subject = "A happy change " + smile;
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
+            .create(admin.newIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
             .to("refs/for/master");
     r.assertOkStatus();
     String id = r.getChangeId();
@@ -3709,7 +3777,7 @@
   @Test
   public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
     exception.expect(BadRequestException.class);
-    exception.expectMessage("no ID was provided and change isn't a revert");
+    exception.expectMessage("revertOf not set");
     gApi.changes().id(createChange().getChangeId()).pureRevert();
   }
 
@@ -3745,7 +3813,7 @@
       u.save();
     }
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
@@ -3759,13 +3827,13 @@
     input.label(label, 1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().keySet())
         .containsExactly(codeReviewLabel, label);
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     // Remove user's permission for 'Label'.
     try (ProjectConfigUpdate u = updateProject(project)) {
       Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
@@ -3777,16 +3845,16 @@
     }
 
     // Verify user's new permitted range.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     change = gApi.changes().id(changeId).get();
     assertPermitted(change, label);
     assertPermitted(change, codeReviewLabel, -1, 0, 1);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(user.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).current().submit();
   }
 
@@ -3829,9 +3897,7 @@
       throws Exception {
     ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS);
     Set<ReviewerState> states =
-        c.reviewers
-            .entrySet()
-            .stream()
+        c.reviewers.entrySet().stream()
             .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get()))
             .map(Map.Entry::getKey)
             .collect(toSet());
@@ -3891,8 +3957,8 @@
       testRepo
           .branch(RefNames.REFS_CONFIG)
           .commit()
-          .author(admin.getIdent())
-          .committer(admin.getIdent())
+          .author(admin.newIdent())
+          .committer(admin.newIdent())
           .add("rules.pl", newContent)
           .message("Modify rules.pl")
           .create();
@@ -3906,7 +3972,7 @@
   public void trackingIds() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT + "\n\n" + "Bug:JRA001",
             PushOneCommit.FILE_NAME,
@@ -3955,25 +4021,41 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     in = new AddReviewerInput();
     in.reviewer = email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).ignore(true);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isTrue();
 
+    // New patch set notification is not sent to users ignoring the change
     sender.clear();
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.changes().id(r.getChangeId()).abandon();
+    requestScopeOperations.setApiUser(admin.id());
+    amendChange(r.getChangeId());
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(new Address(fullname, email));
+    Address address = new Address(fullname, email);
+    assertThat(messages.get(0).rcpt()).containsExactly(address);
 
-    requestScopeOperations.setApiUser(user.getId());
+    // Review notification is not sent to users ignoring the change
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+    messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    assertThat(messages.get(0).rcpt()).containsExactly(address);
+
+    // Abandoned notification is not sent to users ignoring the change
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).abandon();
+    messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    assertThat(messages.get(0).rcpt()).containsExactly(address);
+
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).ignore(false);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse();
   }
@@ -3991,7 +4073,7 @@
   public void cannotIgnoreStarredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().starChange(changeId);
     assertThat(gApi.changes().id(changeId).get().starred).isTrue();
 
@@ -4009,7 +4091,7 @@
   public void cannotStarIgnoredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).ignore(true);
     assertThat(gApi.changes().id(changeId).ignored()).isTrue();
 
@@ -4030,31 +4112,31 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
     gApi.changes().id(r.getChangeId()).markAsReviewed(true);
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
 
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     sender.clear();
     amendChange(r.getChangeId());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user.emailAddress);
+    assertThat(messages.get(0).rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
   public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4079,7 +4161,7 @@
   public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(false);
     assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
 
@@ -4104,7 +4186,7 @@
   public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
     String changeId = createChange().getChangeId();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4133,11 +4215,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());
     }
   }
@@ -4149,4 +4245,13 @@
   private BranchApi createBranch(String branch) throws Exception {
     return createBranch(new Branch.NameKey(project, branch));
   }
+
+  private ThrowableSubject assertThatQueryException(String query) throws Exception {
+    try {
+      query(query);
+    } catch (BadRequestException e) {
+      return assertThat(e);
+    }
+    throw new AssertionError("expected BadRequestException");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
index 3ae38ca..ae88afd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -54,7 +54,7 @@
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushPrivatesWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     result.assertErrorStatus();
   }
 
@@ -64,11 +64,11 @@
   public void pushDraftsWithDisablePrivateChangesTrue() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     result.assertErrorStatus();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     result.assertErrorStatus();
   }
 
@@ -76,7 +76,7 @@
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushWithDisablePrivateChangesTrue() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     result.assertOkStatus();
     assertThat(result.getChange().change().isPrivate()).isFalse();
   }
@@ -85,7 +85,7 @@
   @GerritConfig(name = "change.allowDrafts", value = "true")
   public void pushPrivatesWithDisablePrivateChangesFalse() throws Exception {
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     assertThat(result.getChange().change().isPrivate()).isTrue();
   }
 
@@ -94,11 +94,11 @@
   public void pushDraftsWithDisablePrivateChangesFalse() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     assertThat(result.getChange().change().isPrivate()).isTrue();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     assertThat(result.getChange().change().isPrivate()).isTrue();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
index 81519a7..a08d417 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -55,7 +55,7 @@
     PushOneCommit.Result gp1 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "grand parent 1",
                 ImmutableMap.of("foo", "foo-1.1", "bar", "bar-1.1"))
@@ -65,7 +65,7 @@
     PushOneCommit.Result p1 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 1",
                 ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2"))
@@ -78,7 +78,7 @@
     PushOneCommit.Result gp2 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "grand parent 2",
                 ImmutableMap.of("foo", "foo-2.1", "bar", "bar-2.1"))
@@ -88,7 +88,7 @@
     PushOneCommit.Result p2 =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 "parent 2",
                 ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2"))
@@ -97,7 +97,7 @@
 
     PushOneCommit m =
         pushFactory.create(
-            admin.getIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
     m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
     PushOneCommit.Result result = m.to("refs/for/master");
     result.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
new file mode 100644
index 0000000..d5089ff
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.inject.AbstractModule;
+import org.junit.Test;
+
+@NoHttpd
+public class PluginFieldsIT extends AbstractPluginFieldsTest {
+  // No tests for /detail via the extension API, since the extension API doesn't have that method.
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
+  }
+
+  @Test
+  public void getChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
+  }
+
+  @Test
+  public void getChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()),
+        (id, opts) ->
+            pluginInfoFromSingletonList(
+                gApi.changes().query(id.toString()).withPluginOptions(opts).get()));
+  }
+
+  @Test
+  public void getChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get()),
+        (id, opts) -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get(opts)));
+  }
+
+  static class SimpleAttributeWithExplicitExportModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeAttributeFactory.class)
+          .annotatedWith(Exports.named("simple"))
+          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
+    }
+  }
+
+  @Test
+  public void getChangeWithSimpleAttributeWithExplicitExport() throws Exception {
+    // For backwards compatibility with old plugins, allow modules to bind into the
+    // DynamicSet<ChangeAttributeFactory> as if it were a DynamicMap. We only need one variant of
+    // this test to prove that the mapping works.
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()),
+        SimpleAttributeWithExplicitExportModule.class);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
new file mode 100644
index 0000000..bfcb1a8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -0,0 +1,261 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+public class PrivateChangeIT extends AbstractDaemonTest {
+
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  @Test
+  public void setPrivateByOwner() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    requestScopeOperations.setApiUser(user.id());
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private");
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    gApi.changes().id(changeId).setPrivate(false, null);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+
+    String msg = "This is a security fix that must not be public.";
+    gApi.changes().id(changeId).setPrivate(true, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    msg = "After this security fix has been released we can make it public now.";
+    gApi.changes().id(changeId).setPrivate(false, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+  }
+
+  @Test
+  public void cannotSetMergedChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    approve(result.getChangeId());
+    merge(result);
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("cannot set merged change to private");
+    gApi.changes().id(changeId).setPrivate(true);
+  }
+
+  @Test
+  public void cannotSetAbandonedChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    gApi.changes().id(changeId).abandon();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("cannot set abandoned change to private");
+    gApi.changes().id(changeId).setPrivate(true);
+  }
+
+  @Test
+  public void administratorCanSetUserChangePrivate() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+  }
+
+  @Test
+  public void cannotSetOtherUsersChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    requestScopeOperations.setApiUser(user.id());
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to mark private");
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
+  }
+
+  @Test
+  public void accessPrivate() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
+    // Owner can always access its private changes.
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    // Add admin as a reviewer.
+    gApi.changes().id(result.getChangeId()).addReviewer(admin.id().toString());
+
+    // This change should be visible for admin as a reviewer.
+    requestScopeOperations.setApiUser(admin.id());
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    // Remove admin from reviewers.
+    gApi.changes().id(result.getChangeId()).reviewer(admin.id().toString()).remove();
+
+    // This change should not be visible for admin anymore.
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("Not found: " + result.getChangeId());
+    gApi.changes().id(result.getChangeId());
+  }
+
+  @Test
+  public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
+
+    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+  }
+
+  @Test
+  public void administratorCanUnmarkPrivateAfterMerging() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    merge(result);
+    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+
+    gApi.changes().id(changeId).setPrivate(false, null);
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void ownerCannotMarkPrivateAfterMerging() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    merge(result);
+
+    requestScopeOperations.setApiUser(user.id());
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to mark private");
+    gApi.changes().id(changeId).setPrivate(true, null);
+  }
+
+  @Test
+  public void mergingPrivateChangePublishesIt() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+    approve(result.getChangeId());
+    merge(result);
+
+    assertThat(gApi.changes().id(result.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void ownerCanUnmarkPrivateAfterMerging() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+
+    String changeId = result.getChangeId();
+    gApi.changes().id(changeId).addReviewer(admin.id().toString());
+    merge(result);
+    markMergedChangePrivate(new Change.Id(gApi.changes().id(changeId).get()._number));
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setPrivate(false, null);
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void mergingPrivateChangeThroughGitPublishesIt() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).setPrivate(true);
+
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/heads/master");
+    result.assertOkStatus();
+
+    assertThat(gApi.changes().id(r.getChangeId()).get().isPrivate).isNull();
+  }
+
+  private void markMergedChangePrivate(Change.Id changeId) throws Exception {
+    try (BatchUpdate u =
+        batchUpdateFactory.create(
+            project, identifiedUserFactory.create(admin.id()), TimeUtil.nowTs())) {
+      u.addOp(
+              changeId,
+              new BatchUpdateOp() {
+                @Override
+                public boolean updateChange(ChangeContext ctx) {
+                  ctx.getChange().setPrivate(true);
+                  ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+                  ctx.getChange().setPrivate(true);
+                  ctx.getChange().setLastUpdatedOn(ctx.getWhen());
+                  update.setPrivate(true);
+                  return true;
+                }
+              })
+          .execute();
+    }
+    assertThat(gApi.changes().id(changeId.get()).get().isPrivate).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 2a295b2..bc4dca9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -422,8 +422,8 @@
         testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
     commitBuilder
         .message("New subject " + System.nanoTime())
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     commitBuilder.create();
     GitUtil.pushHead(testRepo, "refs/for/master", false);
     assertThat(getChangeKind(changeId)).isEqualTo(NO_CODE_CHANGE);
@@ -437,8 +437,8 @@
         testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
     commitBuilder
         .message(commitMessage)
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     commitBuilder.create();
     GitUtil.pushHead(testRepo, "refs/for/master", false);
     assertThat(getChangeKind(changeId)).isEqualTo(NO_CHANGE);
@@ -447,7 +447,7 @@
   private void rework(String changeId) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -458,11 +458,11 @@
   }
 
   private void trivialRebase(String changeId) throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     testRepo.reset(getRemoteHead());
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Other Change",
             "a" + System.nanoTime() + ".txt",
@@ -488,7 +488,7 @@
 
     testRepo.reset(parent1.getCommit());
 
-    PushOneCommit merge = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo);
     merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -505,7 +505,7 @@
     testRepo.reset(parent1);
     PushOneCommit.Result newParent1 = createChange("new parent 1", "p1-1.txt", "content 1-1");
 
-    PushOneCommit merge = pushFactory.create(admin.getIdent(), testRepo, changeId);
+    PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo, changeId);
     merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2));
     PushOneCommit.Result result = merge.to("refs/for/master");
     result.assertOkStatus();
@@ -529,7 +529,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                admin.getIdent(),
+                admin.newIdent(),
                 testRepo,
                 PushOneCommit.SUBJECT,
                 "other.txt",
@@ -556,21 +556,21 @@
   }
 
   private void vote(TestAccount user, String changeId, String label, int vote) throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).current().review(new ReviewInput().label(label, vote));
   }
 
   private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote)
       throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in =
         new ReviewInput().label("Code-Review", codeReviewVote).label("Verified", verifiedVote);
     gApi.changes().id(changeId).current().review(in);
   }
 
   private void deleteVote(TestAccount user, String changeId, String label) throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
-    gApi.changes().id(changeId).reviewer(user.getId().toString()).deleteVote(label);
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).reviewer(user.id().toString()).deleteVote(label);
   }
 
   private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote, int verifiedVote) {
@@ -588,7 +588,7 @@
     Integer vote = 0;
     if (c.labels.get(label) != null && c.labels.get(label).all != null) {
       for (ApprovalInfo approval : c.labels.get(label).all) {
-        if (approval._accountId == user.id.get()) {
+        if (approval._accountId == user.id().get()) {
           vote = approval.value;
           break;
         }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 9517bea..6c0e9ab 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -137,7 +137,7 @@
   private PushOneCommit.Result createChange(String dest, String subject) throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             subject,
             "file" + fileCounter.incrementAndGet(),
diff --git a/javatests/com/google/gerrit/acceptance/api/group/BUILD b/javatests/com/google/gerrit/acceptance/api/group/BUILD
index da36a02..a12342a 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/group/BUILD
@@ -21,7 +21,6 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
-        "//lib:gwtorm",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index a664869..d7d311b 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
 import static com.google.gerrit.truth.ListSubject.assertThat;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -35,7 +36,6 @@
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.gerrit.truth.ListSubject;
 import com.google.gerrit.truth.OptionalSubject;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -157,17 +157,17 @@
 
   private void updateGroupWithoutCacheOrIndex(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     groupsUpdate.updateGroupInNoteDb(groupUuid, groupUpdate);
   }
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> updatedGroup) {
-    return assertThat(updatedGroup, InternalGroupSubject::assertThat);
+    return assertThat(updatedGroup, internalGroups());
   }
 
   private static ListSubject<InternalGroupSubject, InternalGroup> assertThatGroups(
       List<InternalGroup> parentGroups) {
-    return assertThat(parentGroups, InternalGroupSubject::assertThat);
+    return assertThat(parentGroups, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index 491cb3a..e269e68 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -64,8 +64,8 @@
     String name1 = groupOperations.newGroup().name("g1").create().get();
     String name2 = groupOperations.newGroup().name("g2").create().get();
 
-    gApi.groups().id(name1).addMembers(user.fullName);
-    gApi.groups().id(name2).addMembers(admin.fullName);
+    gApi.groups().id(name1).addMembers(user.fullName());
+    gApi.groups().id(name2).addMembers(admin.fullName());
     gApi.groups().id(name1).addGroups(name2);
 
     this.g1 = gApi.groups().id(name1).detail();
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index eb0f7e8..47ac7a9 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -31,6 +31,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.Correspondence;
+import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -71,7 +72,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.InternalGroup;
@@ -84,6 +84,9 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.StalenessChecker;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestTimeUtil;
@@ -310,7 +313,7 @@
 
     List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(group.get()).auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, "Registered Users");
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), "Registered Users");
   }
 
   @Test
@@ -410,7 +413,7 @@
 
   @Test
   public void createGroupWithoutCapability_Forbidden() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.groups().create(name("newGroup"));
   }
@@ -730,10 +733,10 @@
   @Test
   public void usersSeeTheirDirectMembershipWhenListingMembersRecursively() throws Exception {
     AccountGroup.UUID group = groupOperations.newGroup().create();
-    gApi.groups().id(group.get()).addMembers(user.username);
+    gApi.groups().id(group.get()).addMembers(user.username());
 
-    requestScopeOperations.setApiUser(user.getId());
-    assertMembers(gApi.groups().id(group.get()).members(true), user.fullName);
+    requestScopeOperations.setApiUser(user.id());
+    assertMembers(gApi.groups().id(group.get()).members(true), user.fullName());
   }
 
   @Test
@@ -741,9 +744,9 @@
     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);
+    gApi.groups().id(group2.get()).addMembers(user.username());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
     assertMembers(listedMembers);
@@ -755,11 +758,11 @@
     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);
+    gApi.groups().id(group2.get()).addMembers(admin.username());
 
     List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
-    assertMembers(listedMembers, admin.fullName);
+    assertMembers(listedMembers, admin.fullName());
   }
 
   @Test
@@ -768,19 +771,19 @@
     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);
+    gApi.groups().id(ownerGroup.get()).addMembers(user.username());
+    gApi.groups().id(group2.get()).addMembers(user.username());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
-    assertMembers(listedMembers, user.fullName);
+    assertMembers(listedMembers, user.fullName());
   }
 
   @Test
   public void defaultGroupsCreated() throws Exception {
     Iterable<String> names = gApi.groups().list().getAsMap().keySet();
-    assertThat(names).containsAllOf("Administrators", "Non-Interactive Users").inOrder();
+    assertThat(names).containsAtLeast("Administrators", "Non-Interactive Users").inOrder();
   }
 
   @Test
@@ -832,13 +835,13 @@
     in.ownerId = adminGroupUuid().get();
     gApi.groups().create(in);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
 
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.groups().id(newGroupName).addMembers(user.username);
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.groups().id(newGroupName).addMembers(user.username());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
   }
 
@@ -911,41 +914,41 @@
     GroupApi g = gApi.groups().create(name("group"));
     List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), admin.id());
 
-    g.addMembers(user.username);
+    g.addMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(2);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), user.id());
 
-    g.removeMembers(user.username);
+    g.removeMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(3);
-    assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id(), user.id());
 
     String otherGroup = name("otherGroup");
     gApi.groups().create(otherGroup);
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(4);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), otherGroup);
 
     g.removeGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(5);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id(), otherGroup);
 
     // Add a removed member back again.
-    g.addMembers(user.username);
+    g.addMembers(user.username());
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(6);
-    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id(), user.id());
 
     // Add a removed group back again.
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(7);
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), otherGroup);
 
     Timestamp lastDate = null;
     for (GroupAuditEventInfo auditEvent : auditEvents) {
@@ -977,7 +980,7 @@
     List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(parentGroup.id).auditLog();
     assertThat(auditEvents).hasSize(2);
     // Verify the unavailable subgroup's name is null.
-    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, null);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id(), null);
   }
 
   private void deleteGroupRef(String groupId) throws Exception {
@@ -1006,20 +1009,20 @@
     TestAccount groupOwner = accountCreator.user2();
     GroupInput in = new GroupInput();
     in.name = name("group");
-    in.members = Stream.of(groupOwner).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
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.groups().id(group.id).index();
 
     // group owner can reindex own group (group is owned by itself)
-    requestScopeOperations.setApiUser(groupOwner.getId());
+    requestScopeOperations.setApiUser(groupOwner.id());
     gApi.groups().id(group.id).index();
 
     // user cannot reindex any group
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to index group");
     gApi.groups().id(group.id).index();
@@ -1074,11 +1077,15 @@
 
   private void assertPushToGroupBranch(
       Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception {
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS);
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      ProjectConfig cfg = u.getConfig();
+      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
+      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
+      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
+      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
+      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPNAMES);
+      u.save();
+    }
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
 
@@ -1087,7 +1094,7 @@
     repo.reset("groupRef");
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
+            .create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .to(groupRefName);
     if (expectedErrorOnUpdate != null) {
       r.assertErrorStatus(expectedErrorOnUpdate);
@@ -1097,12 +1104,16 @@
   }
 
   private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
-    grant(project, RefNames.REFS_GROUPS + "*", 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 + "*");
+      u.save();
+    }
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
+            .create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .setParents(ImmutableList.of())
             .to(RefNames.REFS_GROUPS + name("bar"));
     r.assertOkStatus();
@@ -1140,7 +1151,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), repo, "Subject", "project.config", config)
+            .create(admin.newIdent(), repo, "Subject", "project.config", config)
             .to(RefNames.REFS_CONFIG);
     r.assertErrorStatus("invalid project configuration");
     r.assertMessage("All-Users must inherit from All-Projects");
@@ -1190,7 +1201,7 @@
     grant(allUsers, refPattern, Permission.PUSH);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.getIdent(), allUsersRepo).to(groupRef);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(groupRef);
     r.assertErrorStatus();
     assertThat(r.getMessage()).contains("Not allowed to create group branch.");
 
@@ -1289,12 +1300,12 @@
   public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
     GroupInput groupInput = new GroupInput();
     groupInput.name = name("contributors");
-    groupInput.members = ImmutableList.of(user.username);
+    groupInput.members = ImmutableList.of(user.username());
     gApi.groups().create(groupInput).get();
     restartAsSlave();
 
-    requestScopeOperations.setApiUser(user.getId());
-    List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get();
+    requestScopeOperations.setApiUser(user.id());
+    List<GroupInfo> groups = gApi.groups().list().withUser(user.username()).get();
     ImmutableList<String> groupNames =
         groups.stream().map(group -> group.name).collect(toImmutableList());
     assertThat(groupNames).contains(groupInput.name);
@@ -1383,18 +1394,15 @@
   }
 
   private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() {
-    return new Correspondence<AccountInfo, String>() {
-      @Override
-      public boolean compare(AccountInfo actualAccount, String expectedName) {
-        String username = actualAccount == null ? null : actualAccount.username;
-        return Objects.equals(username, expectedName);
-      }
-
-      @Override
-      public String toString() {
-        return "has username";
-      }
-    };
+    return Correspondence.from(
+        new BinaryPredicate<AccountInfo, String>() {
+          @Override
+          public boolean apply(AccountInfo actualAccount, String expectedName) {
+            String username = actualAccount == null ? null : actualAccount.username;
+            return Objects.equals(username, expectedName);
+          }
+        },
+        "has username");
   }
 
   private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
@@ -1419,7 +1427,7 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), repo, "Update group config", "group.config", "some content")
+            .create(admin.newIdent(), repo, "Update group config", "group.config", "some content")
             .to(MagicBranch.NEW_CHANGE + groupRef);
     r.assertOkStatus();
     assertThat(r.getChange().change().getDest().get()).isEqualTo(groupRef);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index 7056312..5e143c0 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -18,7 +18,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.ServerInitiated;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -92,7 +92,7 @@
   }
 
   private void createGroup(InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
   }
 
@@ -138,7 +138,7 @@
       InternalGroupUpdate groupUpdate = InternalGroupUpdate.builder().build();
       try {
         groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
-      } catch (OrmException | IOException | ConfigInvalidException e) {
+      } catch (StorageException | IOException | ConfigInvalidException e) {
         throw new IllegalStateException(e);
       }
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 752d5f1..1d3eb17 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -116,7 +116,7 @@
     deprecatedInput();
 
     // Non-admin cannot disable
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       gApi.plugins().name("plugin-a").disable();
       fail("Expected AuthException");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index f0296fc..8cdd2f66 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -32,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;
@@ -58,28 +60,31 @@
         groupOperations.newGroup().name(name("privilegedGroup")).create();
 
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
-    groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id).update();
+    groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
 
-    grant(secretProject, "refs/*", Permission.READ, false, privilegedGroupUuid);
-    block(secretProject, "refs/*", Permission.READ, SystemGroupBackend.REGISTERED_USERS);
+    try (ProjectConfigUpdate u = updateProject(secretProject)) {
+      ProjectConfig cfg = u.getConfig();
+      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/*");
+      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/*");
+      u.save();
+    }
 
-    deny(secretRefProject, "refs/*", Permission.READ, SystemGroupBackend.ANONYMOUS_USERS);
-    grant(secretRefProject, "refs/heads/secret/*", Permission.READ, false, privilegedGroupUuid);
-    block(
-        secretRefProject,
-        "refs/heads/secret/*",
-        Permission.READ,
-        SystemGroupBackend.REGISTERED_USERS);
-    grant(
-        secretRefProject,
-        "refs/heads/*",
-        Permission.READ,
-        false,
-        SystemGroupBackend.REGISTERED_USERS);
+    try (ProjectConfigUpdate u = updateProject(secretRefProject)) {
+      ProjectConfig cfg = u.getConfig();
+      Util.deny(cfg, Permission.READ, SystemGroupBackend.ANONYMOUS_USERS, "refs/*");
+      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/heads/secret/*");
+      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/secret/*");
+      Util.allow(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/*");
+      u.save();
+    }
 
     // Ref permission
-    grant(normalProject, "refs/*", Permission.VIEW_PRIVATE_CHANGES, false, privilegedGroupUuid);
-    grant(normalProject, "refs/*", Permission.FORGE_SERVER, false, privilegedGroupUuid);
+    try (ProjectConfigUpdate u = updateProject(normalProject)) {
+      ProjectConfig cfg = u.getConfig();
+      Util.allow(cfg, Permission.VIEW_PRIVATE_CHANGES, privilegedGroupUuid, "refs/*");
+      Util.allow(cfg, Permission.FORGE_SERVER, privilegedGroupUuid, "refs/*");
+      u.save();
+    }
   }
 
   @Test
@@ -92,7 +97,7 @@
   @Test
   public void nonexistentPermission() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "notapermission";
     in.ref = "refs/heads/master";
 
@@ -104,7 +109,7 @@
   @Test
   public void permissionLacksRef() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "forge_author";
 
     exception.expect(BadRequestException.class);
@@ -115,7 +120,7 @@
   @Test
   public void changePermission() throws Exception {
     AccessCheckInput in = new AccessCheckInput();
-    in.account = user.email;
+    in.account = user.email();
     in.permission = "rebase";
     in.ref = "refs/heads/master";
 
@@ -132,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);
   }
 
@@ -182,7 +187,7 @@
                 + normalProject.get()
                 + "/check.access"
                 + "?ref=refs/heads/master&perm=viewPrivateChanges&account="
-                + user.email);
+                + user.email());
     rep.assertOK();
     assertThat(rep.getEntityContent()).contains("403");
   }
@@ -192,28 +197,28 @@
     List<TestCase> inputs =
         ImmutableList.of(
             TestCase.projectRefPerm(
-                user.email,
+                user.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.VIEW_PRIVATE_CHANGES,
                 403),
-            TestCase.project(user.email, normalProject.get(), 200),
-            TestCase.project(user.email, secretProject.get(), 403),
+            TestCase.project(user.email(), normalProject.get(), 200),
+            TestCase.project(user.email(), secretProject.get(), 403),
             TestCase.projectRef(
-                user.email, secretRefProject.get(), "refs/heads/secret/master", 403),
+                user.email(), secretRefProject.get(), "refs/heads/secret/master", 403),
             TestCase.projectRef(
-                privilegedUser.email, secretRefProject.get(), "refs/heads/secret/master", 200),
-            TestCase.projectRef(privilegedUser.email, normalProject.get(), null, 200),
-            TestCase.projectRef(privilegedUser.email, secretProject.get(), null, 200),
-            TestCase.projectRef(privilegedUser.email, secretProject.get(), null, 200),
+                privilegedUser.email(), secretRefProject.get(), "refs/heads/secret/master", 200),
+            TestCase.projectRef(privilegedUser.email(), normalProject.get(), null, 200),
+            TestCase.projectRef(privilegedUser.email(), secretProject.get(), null, 200),
+            TestCase.projectRef(privilegedUser.email(), secretProject.get(), null, 200),
             TestCase.projectRefPerm(
-                privilegedUser.email,
+                privilegedUser.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.VIEW_PRIVATE_CHANGES,
                 200),
             TestCase.projectRefPerm(
-                privilegedUser.email,
+                privilegedUser.email(),
                 normalProject.get(),
                 "refs/heads/master",
                 Permission.FORGE_SERVER,
@@ -261,7 +266,7 @@
       assertThat(u.delete()).isEqualTo(Result.FORCED);
     }
     AccessCheckInput input = new AccessCheckInput();
-    input.account = privilegedUser.email;
+    input.account = privilegedUser.email();
 
     AccessCheckInfo info = gApi.projects().name(normalProject.get()).checkAccess(input);
     assertThat(info.status).isEqualTo(200);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 6c6ad3d..388ea30 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -78,10 +78,7 @@
     CheckProjectResultInfo checkResult =
         gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toList()))
         .containsExactly(change._number);
@@ -106,10 +103,7 @@
     input.autoCloseableChangesCheck.fix = true;
     CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toSet()))
         .containsExactly(change._number);
@@ -132,10 +126,7 @@
     CheckProjectResultInfo checkResult =
         gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toSet()))
         .containsExactly(r.getChange().getId().get());
@@ -159,10 +150,7 @@
     input.autoCloseableChangesCheck.fix = true;
     CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toSet()))
         .containsExactly(r.getChange().getId().get());
@@ -196,10 +184,7 @@
     input.autoCloseableChangesCheck.maxCommits = 2;
     checkResult = gApi.projects().name(project.get()).check(input);
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toSet()))
         .containsExactly(r.getChange().getId().get());
@@ -233,10 +218,7 @@
     input.autoCloseableChangesCheck.skipCommits = 1;
     checkResult = gApi.projects().name(project.get()).check(input);
     assertThat(
-            checkResult
-                .autoCloseableChangesCheckResult
-                .autoCloseableChanges
-                .stream()
+            checkResult.autoCloseableChangesCheckResult.autoCloseableChanges.stream()
                 .map(i -> i._number)
                 .collect(toSet()))
         .containsExactly(r.getChange().getId().get());
@@ -298,8 +280,8 @@
             .branch("HEAD")
             .commit()
             .message("A change")
-            .author(admin.getIdent())
-            .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()))
+            .author(admin.newIdent())
+            .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
             .create();
     pushHead(testRepo, "refs/for/master");
     return commit;
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 9267bc3..77350b0 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -353,7 +353,7 @@
   @Test
   public void nonOwnerCannotSetConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("write refs/meta/config not permitted");
     gApi.projects().name(project.get()).config(input);
@@ -388,7 +388,7 @@
   @Test
   public void setHeadNotAllowed() throws Exception {
     gApi.projects().name(project.get()).branch("test").create(new BranchInput());
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted: set HEAD on refs/heads/test");
     gApi.projects().name(project.get()).head("test");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index e428e89..3c1428d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -41,7 +41,7 @@
   @Test
   public void setParentNotAllowed() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -50,7 +50,7 @@
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentNotAllowedForNonOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -74,7 +74,7 @@
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedForOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index b6969a2..cbfc09f 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -415,7 +415,7 @@
         .content()
         .onlyElement()
         .commonLines()
-        .containsAllOf("Line 1", "Line 2", "Line 3")
+        .containsExactly("Line 1", "Line 2", "Line 3", "")
         .inOrder();
     assertThat(diffInfo).content().onlyElement().linesOfA().isNull();
     assertThat(diffInfo).content().onlyElement().linesOfB().isNull();
@@ -2363,7 +2363,7 @@
         .content()
         .element(0)
         .commonLines()
-        .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
         .inOrder();
   }
 
@@ -2389,7 +2389,7 @@
         .content()
         .element(0)
         .commonLines()
-        .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+        .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
         .inOrder();
   }
 
@@ -2521,7 +2521,7 @@
       throws Exception {
     testRepo.reset(parentCommit);
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "Adjust files of repo", files);
+        pushFactory.create(admin.newIdent(), testRepo, "Adjust files of repo", files);
     PushOneCommit.Result result = push.to("refs/for/master");
     return result.getCommit();
   }
@@ -2545,7 +2545,7 @@
         Arrays.stream(removedFilePaths)
             .collect(toMap(Function.identity(), path -> "Irrelevant content"));
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "Remove files from repo", files);
+        pushFactory.create(admin.newIdent(), testRepo, "Remove files from repo", files);
     PushOneCommit.Result result = push.rm("refs/for/master");
     return result.getCommit();
   }
@@ -2564,7 +2564,7 @@
 
   private Result createEmptyChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "Test change", ImmutableMap.of());
+        pushFactory.create(admin.newIdent(), testRepo, "Test change", ImmutableMap.of());
     return push.to("refs/for/master");
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index c87f7d1..fc65eca 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -35,6 +35,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
@@ -217,42 +218,32 @@
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     revision(r).review(ReviewInput.approve());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     revision(r).review(ReviewInput.recommend());
 
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.changes().id(changeId).reviewer(user.username).deleteVote("Code-Review");
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes().id(changeId).reviewer(user.username()).deleteVote("Code-Review");
     Optional<ApprovalInfo> crUser =
-        get(changeId, DETAILED_LABELS)
-            .labels
-            .get("Code-Review")
-            .all
-            .stream()
-            .filter(a -> a._accountId == user.id.get())
+        get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
+            .filter(a -> a._accountId == user.id().get())
             .findFirst();
     assertThat(crUser).isPresent();
     assertThat(crUser.get().value).isEqualTo(0);
 
     revision(r).submit();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput in = new ReviewInput();
     in.label("Code-Review", 1);
     in.message = "Still LGTM";
     revision(r).review(in);
 
     ApprovalInfo cr =
-        gApi.changes()
-            .id(changeId)
-            .get(DETAILED_LABELS)
-            .labels
-            .get("Code-Review")
-            .all
-            .stream()
-            .filter(a -> a._accountId == user.getId().get())
+        gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
+            .filter(a -> a._accountId == user.id().get())
             .findFirst()
             .get();
     assertThat(cr.postSubmit).isTrue();
@@ -304,7 +295,7 @@
   @Test
   public void voteNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("is restricted");
     gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
@@ -421,7 +412,7 @@
     String subject = "Test change\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
+            admin.newIdent(), testRepo, subject, "another_file.txt", "another content");
     PushOneCommit.Result r2 = push.to("refs/for/master");
 
     // Change 2's parent should be change 1
@@ -478,7 +469,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -504,7 +495,7 @@
     String destContent = "some content";
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             ImmutableMap.of(PushOneCommit.FILE_NAME, destContent, "foo.txt", "foo"));
@@ -515,7 +506,7 @@
     String changeContent = "another content";
     push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             ImmutableMap.of(PushOneCommit.FILE_NAME, changeContent, "bar.txt", "bar"));
@@ -586,15 +577,14 @@
             "Patch Set 1: Cherry Picked from branch master.\n\n"
                 + "The following files contain Git conflicts:\n"
                 + "* "
-                + PushOneCommit.FILE_NAME
-                + "\n");
+                + PushOneCommit.FILE_NAME);
   }
 
   @Test
   public void cherryPickToExistingChange() throws Exception {
     PushOneCommit.Result r1 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "a")
             .to("refs/for/master");
     String t1 = project.get() + "~master~" + r1.getChangeId();
 
@@ -604,7 +594,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
             .to("refs/for/foo");
     String t2 = project.get() + "~foo~" + r2.getChangeId();
     gApi.changes().id(t2).abandon();
@@ -738,7 +728,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.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     CherryPickInput input = new CherryPickInput();
     input.message = "it goes to a new branch";
 
@@ -761,7 +751,7 @@
     input.destination = "branch-3";
     input.notify = NotifyHandling.NONE;
     input.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
     sender.clear();
     gApi.changes().id(changeId).current().cherryPick(input);
     assertNotifyTo(userToNotify);
@@ -774,13 +764,13 @@
     // Change is created by 'admin'.
     PushOneCommit.Result r = createChange();
     // Change is approved by 'admin2'. Change is CC'd to 'user'.
-    requestScopeOperations.setApiUser(accountCreator.admin2().getId());
+    requestScopeOperations.setApiUser(accountCreator.admin2().id());
     ReviewInput in = ReviewInput.approve();
-    in.reviewer(user.email, ReviewerState.CC, true);
+    in.reviewer(user.email(), ReviewerState.CC, true);
     gApi.changes().id(r.getChangeId()).current().review(in);
 
     // Change is cherrypicked by 'user2'.
-    requestScopeOperations.setApiUser(accountCreator.user2().getId());
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
     CherryPickInput cin = new CherryPickInput();
     cin.message = "this need to go to stable";
     cin.destination = "stable";
@@ -797,8 +787,8 @@
     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());
+    assertThat(ccs).containsExactly(user.id().get());
+    assertThat(reviewers).containsExactly(admin.id().get(), accountCreator.admin2().id().get());
   }
 
   @Test
@@ -859,7 +849,7 @@
     input.base = dstChange.getCommit().name();
     input.message = srcChange.getCommit().getFullMessage();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(
         String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
@@ -880,8 +870,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);
   }
 
@@ -919,11 +909,11 @@
 
   @Test
   public void canRebase() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r2 = push.to("refs/for/master");
     boolean canRebase =
         gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).canRebase();
@@ -931,7 +921,7 @@
     merge(r2);
 
     testRepo.reset(r1.getCommit());
-    push = pushFactory.create(admin.getIdent(), testRepo);
+    push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r3 = push.to("refs/for/master");
 
     canRebase = gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).canRebase();
@@ -940,7 +930,7 @@
 
   @Test
   public void setUnsetReviewedFlag() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes().id(r.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
@@ -954,12 +944,27 @@
   }
 
   @Test
+  public void setUnsetReviewedFlagByFileApi() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+
+    gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(true);
+
+    assertThat(Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).current().reviewed()))
+        .isEqualTo(PushOneCommit.FILE_NAME);
+
+    gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(false);
+
+    assertThat(gApi.changes().id(r.getChangeId()).current().reviewed()).isEmpty();
+  }
+
+  @Test
   public void mergeable() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
 
     PushOneCommit push1 =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -974,7 +979,7 @@
 
     PushOneCommit push2 =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME,
@@ -1093,7 +1098,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(admin.getIdent(), testRepo, SUBJECT, files).to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
     result.assertOkStatus();
     String changeId = result.getChangeId();
 
@@ -1125,7 +1130,7 @@
   public void setDescriptionNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("edit description not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
@@ -1136,7 +1141,7 @@
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
     grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
   }
@@ -1260,7 +1265,7 @@
                 .get()
                 .author
                 .email)
-        .isEqualTo(admin.email);
+        .isEqualTo(admin.email());
 
     draftApi.delete();
     assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).drafts())
@@ -1286,7 +1291,7 @@
     assertThat(out).hasSize(1);
     CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
     assertThat(comment.message).isEqualTo(in.message);
-    assertThat(comment.author.email).isEqualTo(admin.email);
+    assertThat(comment.author.email).isEqualTo(admin.email());
     assertThat(comment.path).isNull();
 
     List<CommentInfo> list =
@@ -1399,17 +1404,17 @@
     amendChange(r.getChangeId());
 
     // code-review
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
     // check if it's blocked to delete a vote on a non-current patch set.
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     exception.expect(MethodNotAllowedException.class);
     exception.expectMessage("Cannot access on non-current patch set");
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().getName())
-        .reviewer(user.getId().toString())
+        .reviewer(user.id().toString())
         .deleteVote("Code-Review");
   }
 
@@ -1422,27 +1427,68 @@
     amendChange(r.getChangeId());
 
     // code-review
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes()
         .id(r.getChangeId())
         .current()
-        .reviewer(user.getId().toString())
+        .reviewer(user.id().toString())
         .deleteVote("Code-Review");
 
     Map<String, Short> m =
-        gApi.changes().id(r.getChangeId()).current().reviewer(user.getId().toString()).votes();
+        gApi.changes().id(r.getChangeId()).current().reviewer(user.id().toString()).votes();
 
     assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 0));
 
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(ReviewerState.REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
+  }
+
+  @Test
+  public void listVotesByRevision() throws Exception {
+    // Create patch set 1 and vote on it
+    String changeId = createChange().getChangeId();
+    ListMultimap<String, ApprovalInfo> votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes).isEmpty();
+    recommend(changeId);
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    List<ApprovalInfo> approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(1);
+    ApprovalInfo approval = approvals.get(0);
+    assertThat(approval._accountId).isEqualTo(admin.id().get());
+    assertThat(approval.email).isEqualTo(admin.email());
+    assertThat(approval.username).isEqualTo(admin.username());
+
+    // Also vote on it with another user
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+    // Patch set 1 has 2 votes on Code-Review
+    requestScopeOperations.setApiUser(admin.id());
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(2);
+    assertThat(approvals.stream().map(a -> a._accountId))
+        .containsExactlyElementsIn(ImmutableList.of(admin.id().get(), user.id().get()));
+
+    // Create a new patch set which does not have any votes
+    amendChange(changeId);
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes).isEmpty();
+
+    // Votes are still returned for ps 1
+    votes = gApi.changes().id(changeId).revision(1).votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(2);
   }
 
   private static void assertCherryPickResult(
@@ -1459,7 +1505,7 @@
       throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
+            admin.newIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
     return push.to("refs/for/master");
   }
 
@@ -1485,18 +1531,18 @@
 
     PushOneCommit.Result changeAResult =
         pushFactory
-            .create(admin.getIdent(), testRepo, "change a", parent1FileName, "Content of a")
+            .create(admin.newIdent(), testRepo, "change a", parent1FileName, "Content of a")
             .to("refs/for/" + branchAName);
 
     testRepo.reset(initialCommit);
     PushOneCommit.Result changeBResult =
         pushFactory
-            .create(admin.getIdent(), testRepo, "change b", parent2FileName, "Content of b")
+            .create(admin.newIdent(), testRepo, "change b", parent2FileName, "Content of b")
             .to("refs/for/" + branchBName);
 
     PushOneCommit pushableMergeCommit =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "merge",
             ImmutableMap.of(parent1FileName, "Content of a", parent2FileName, "Content of b"));
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 90f0a2a..ba228f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -24,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;
@@ -68,7 +67,7 @@
   public void setUp() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Provide files which can be used for fixes",
             ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2));
@@ -106,7 +105,7 @@
     RobotCommentInput in = createRobotCommentInput();
     addRobotComment(changeId, in);
 
-    pushFactory.create(admin.getIdent(), testRepo, changeId).to("refs/for/master");
+    pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master");
 
     RobotCommentInput in2 = createRobotCommentInput();
     addRobotComment(changeId, in2);
@@ -918,21 +917,18 @@
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
     addRobotComment(r2.getChangeId(), createRobotCommentInputWithMandatoryFields());
 
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
+    try (AutoCloseable ignored = disableNoteDb()) {
       ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId()));
       // currently, we create all robot comments as 'resolved' by default.
       // if we allow users to resolve a robot comment, then this test should
       // be modified.
       assertThat(result.unresolvedCommentCount).isEqualTo(0);
       assertThat(result.totalCommentCount).isEqualTo(1);
-    } finally {
-      enableDb(ctx);
     }
   }
 
@@ -1010,7 +1006,7 @@
     assertThat(c.line).isEqualTo(expected.line);
     assertThat(c.message).isEqualTo(expected.message);
 
-    assertThat(c.author.email).isEqualTo(admin.email);
+    assertThat(c.author.email).isEqualTo(admin.email());
 
     if (expectPath) {
       assertThat(c.path).isEqualTo(expected.path);
@@ -1028,8 +1024,7 @@
 
   private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
     assertThatList(robotComments).isNotNull();
-    return robotComments
-        .stream()
+    return robotComments.stream()
         .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
         .filter(Objects::nonNull)
         .flatMap(List::stream)
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 85b66c2..6827219 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -22,6 +22,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static com.google.gerrit.extensions.restapi.testing.BinaryResultSubject.assertThat;
+import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -43,6 +44,7 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeEditDetailOption;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -52,7 +54,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -110,11 +111,11 @@
 
   @Before
   public void setUp() throws Exception {
-    changeId = newChange(admin.getIdent());
+    changeId = newChange(admin.newIdent());
     ps = getCurrentPatchSet(changeId);
     assertThat(ps).isNotNull();
-    amendChange(admin.getIdent(), changeId);
-    changeId2 = newChange2(admin.getIdent());
+    amendChange(admin.newIdent(), changeId);
+    changeId2 = newChange2(admin.newIdent());
   }
 
   @Test
@@ -138,7 +139,7 @@
   @Test
   public void deleteEditOfOlderPatchSet() throws Exception {
     createArbitraryEditFor(changeId2);
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
 
     gApi.changes().id(changeId2).edit().delete();
     assertThat(getEdit(changeId2)).isAbsent();
@@ -198,7 +199,7 @@
   @Test
   public void publishEditNotifyRest() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     createArbitraryEditFor(changeId);
@@ -213,7 +214,7 @@
   @Test
   public void publishEditWithDefaultNotify() throws Exception {
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     createArbitraryEditFor(changeId);
@@ -235,7 +236,7 @@
     PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
@@ -254,7 +255,7 @@
     PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-    amendChange(admin.getIdent(), changeId2);
+    amendChange(admin.newIdent(), changeId2);
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
 
     Optional<EditInfo> originalEdit = getEdit(changeId2);
@@ -277,7 +278,7 @@
     assertThat(edit).value().baseRevision().isEqualTo(currentPatchSet.getRevision().get());
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             FILE_NAME,
@@ -301,7 +302,7 @@
   public void updateRootCommitMessage() throws Exception {
     // Re-clone empty repo; TestRepository doesn't let us reset to unborn head.
     testRepo = cloneProject(project);
-    changeId = newChange(admin.getIdent());
+    changeId = newChange(admin.newIdent());
 
     createEmptyEditFor(changeId);
     Optional<EditInfo> edit = getEdit(changeId);
@@ -400,7 +401,9 @@
   public void retrieveEdit() throws Exception {
     adminRestSession.get(urlEdit(changeId)).assertNoContent();
     createArbitraryEditFor(changeId);
-    EditInfo editInfo = getEditInfo(changeId, false);
+    Optional<EditInfo> maybeEditInfo = gApi.changes().id(changeId).edit().get();
+    assertThat(maybeEditInfo).isPresent();
+    EditInfo editInfo = maybeEditInfo.get();
     ChangeInfo changeInfo = get(changeId, CURRENT_REVISION, CURRENT_COMMIT);
     assertThat(editInfo.commit.commit).isNotEqualTo(changeInfo.currentRevision);
     assertThat(editInfo).commit().parents().hasSize(1);
@@ -414,11 +417,7 @@
   @Test
   public void retrieveFilesInEdit() throws Exception {
     createEmptyEditFor(changeId);
-    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-
-    EditInfo info = getEditInfo(changeId, true);
-    assertThat(info.files).isNotNull();
-    assertThat(info.files.keySet()).containsExactly(Patch.COMMIT_MSG, FILE_NAME, FILE_NAME2);
+    assertFiles(changeId, ImmutableList.of(COMMIT_MSG, FILE_NAME, FILE_NAME2));
   }
 
   @Test
@@ -559,8 +558,10 @@
   @Test
   public void addNewFile() throws Exception {
     createEmptyEditFor(changeId);
+    assertFiles(changeId, ImmutableList.of(COMMIT_MSG, FILE_NAME, FILE_NAME2));
     gApi.changes().id(changeId).edit().modifyFile(FILE_NAME3, RawInputUtil.create(CONTENT_NEW));
     ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_NEW);
+    assertFiles(changeId, ImmutableList.of(COMMIT_MSG, FILE_NAME, FILE_NAME2, FILE_NAME3));
   }
 
   @Test
@@ -627,11 +628,11 @@
     gApi.changes().id(changeId2).edit().publish(publishInput);
     assertThat(queryEdits()).isEmpty();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     createEmptyEditFor(changeId);
     assertThat(queryEdits()).hasSize(1);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     assertThat(queryEdits()).isEmpty();
   }
 
@@ -680,7 +681,7 @@
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
 
     // Create change as user
-    PushOneCommit push = pushFactory.create(user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
     r1.assertOkStatus();
 
@@ -767,6 +768,19 @@
     assertThat(fileContent).value().bytes().isEqualTo(expectedFileBytes);
   }
 
+  private void assertFiles(String changeId, List<String> expected) throws Exception {
+    Optional<EditInfo> info =
+        gApi.changes()
+            .id(changeId)
+            .edit()
+            .detail()
+            .withOption(ChangeEditDetailOption.LIST_FILES)
+            .get();
+    assertThat(info).isPresent();
+    assertThat(info.get().files).isNotNull();
+    assertThat(info.get().files.keySet()).containsExactlyElementsIn(expected);
+  }
+
   private String urlEdit(String changeId) {
     return "/changes/" + changeId + "/edit";
   }
@@ -783,10 +797,6 @@
     return urlEdit(changeId) + "/" + fileName + (base ? "?base" : "");
   }
 
-  private String urlGetFiles(String changeId) {
-    return urlEdit(changeId) + "?list";
-  }
-
   private String urlRevisionFiles(String changeId, String revisionId) {
     return "/changes/" + changeId + "/revisions/" + revisionId + "/files";
   }
@@ -821,11 +831,6 @@
         + "/diff?context=ALL&intraline";
   }
 
-  private EditInfo getEditInfo(String changeId, boolean files) throws Exception {
-    RestResponse r = adminRestSession.get(files ? urlGetFiles(changeId) : urlEdit(changeId));
-    return readContentFromJson(r, EditInfo.class);
-  }
-
   private <T> T readContentFromJson(RestResponse r, Class<T> clazz) throws Exception {
     r.assertOK();
     try (JsonReader jsonReader = new JsonReader(r.getReader())) {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 0fc3c80..899b09a 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -50,6 +50,7 @@
 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;
@@ -125,6 +126,7 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+@SkipProjectClone
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
     // TODO(dborowitz): TEST.
@@ -134,6 +136,7 @@
 
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  private static String NEW_CHANGE_INDICATOR = " [NEW]";
   private LabelType patchSetLock;
 
   @BeforeClass
@@ -166,10 +169,10 @@
 
   @After
   public void resetPublishCommentOnPushOption() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    requestScopeOperations.setApiUser(admin.id());
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = false;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   protected void selectProtocol(Protocol p) throws Exception {
@@ -317,8 +320,8 @@
         testRepo
             .commit()
             .message("Initial commit")
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .insertChangeId()
             .create();
     String id = GitUtil.getChangeId(testRepo, c).get();
@@ -350,8 +353,8 @@
         testRepo
             .commit()
             .message("Initial commit")
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .insertChangeId()
             .create();
     testRepo.reset(c);
@@ -374,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";
@@ -386,7 +389,7 @@
         .create();
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .create(admin.newIdent(), testRepo, "another commit", "b.txt", "bbb")
             .to("refs/for/master");
     Change.Id id2 = r2.getChange().getId();
     r2.assertOkStatus();
@@ -394,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");
   }
 
@@ -437,8 +439,8 @@
         .branch("HEAD")
         .commit()
         .message("A change")
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()))
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
         .create();
     PushResult result = pushHead(testRepo, "refs/for/master");
     assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated");
@@ -491,7 +493,7 @@
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add(topicOption);
 
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
 
@@ -524,11 +526,11 @@
     pwi.filter = "*";
     pwi.notifyNewChanges = true;
     projectsToWatch.add(pwi);
-    requestScopeOperations.setApiUser(user3.getId());
+    requestScopeOperations.setApiUser(user3.id());
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
 
     TestAccount user2 = accountCreator.user2();
-    String pushSpec = "refs/for/master%reviewer=" + user.email + ",cc=" + user2.email;
+    String pushSpec = "refs/for/master%reviewer=" + user.email() + ",cc=" + user2.email();
 
     sender.clear();
     PushOneCommit.Result r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE);
@@ -546,43 +548,44 @@
     r.assertOkStatus();
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
 
     sender.clear();
     r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
     r.assertOkStatus();
     assertThat(sender.getMessages()).hasSize(1);
     m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress, user3.emailAddress);
+    assertThat(m.rcpt())
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress(), user3.getEmailAddress());
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + user3.email());
     r.assertOkStatus();
     assertNotifyTo(user3);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + user3.email());
     r.assertOkStatus();
     assertNotifyCc(user3);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + user3.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + user3.email());
     r.assertOkStatus();
     assertNotifyBcc(user3);
 
     // request that sender gets notified as TO, CC and BCC, email should be sent
     // even if the sender is the only recipient
     sender.clear();
-    pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + admin.email);
+    pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + admin.email());
     assertNotifyTo(admin);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + admin.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + admin.email());
     r.assertOkStatus();
     assertNotifyCc(admin);
 
     sender.clear();
-    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + admin.email);
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + admin.email());
     r.assertOkStatus();
     assertNotifyBcc(admin);
   }
@@ -591,7 +594,7 @@
   public void pushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user));
 
@@ -601,11 +604,11 @@
             "refs/for/master/"
                 + topic
                 + "%cc="
-                + admin.email
+                + admin.email()
                 + ",cc="
-                + user.email
+                + user.email()
                 + ",cc="
-                + accountCreator.user2().email);
+                + accountCreator.user2().email());
     r.assertOkStatus();
     // Check that admin isn't CC'd as they own the change
     r.assertChange(
@@ -621,11 +624,11 @@
             "refs/for/master/"
                 + topic
                 + "%cc="
-                + admin.email
+                + admin.email()
                 + ",cc="
                 + nonExistingEmail
                 + ",cc="
-                + user.email);
+                + user.email());
     r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
   }
 
@@ -641,8 +644,7 @@
 
     ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
     ImmutableList<AccountInfo> ccs =
-        firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>of())
-            .stream()
+        firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>of()).stream()
             .sorted(comparing((AccountInfo a) -> a.email))
             .collect(toImmutableList());
     assertThat(ccs).hasSize(2);
@@ -658,7 +660,7 @@
     String group = name("group");
     GroupInput gin = new GroupInput();
     gin.name = group;
-    gin.members = ImmutableList.of(user.username, user2.username);
+    gin.members = ImmutableList.of(user.username(), user2.username());
     gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
     gApi.groups().create(gin);
 
@@ -671,7 +673,7 @@
   public void pushForMasterWithReviewer() throws Exception {
     // add one reviewer
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, user);
 
@@ -683,11 +685,11 @@
             "refs/for/master/"
                 + topic
                 + "%r="
-                + admin.email
+                + admin.email()
                 + ",r="
-                + user.email
+                + user.email()
                 + ",r="
-                + user2.email);
+                + user2.email());
     r.assertOkStatus();
     // admin is the owner of the change and should not appear as reviewer
     r.assertChange(Change.Status.NEW, topic, user, user2);
@@ -699,11 +701,11 @@
             "refs/for/master/"
                 + topic
                 + "%r="
-                + admin.email
+                + admin.email()
                 + ",r="
                 + nonExistingEmail
                 + ",r="
-                + user.email);
+                + user.email());
     r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
   }
 
@@ -736,7 +738,7 @@
     String group = name("group");
     GroupInput gin = new GroupInput();
     gin.name = group;
-    gin.members = ImmutableList.of(user.username, user2.username);
+    gin.members = ImmutableList.of(user.username(), user2.username());
     gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
     gApi.groups().create(gin);
 
@@ -834,9 +836,9 @@
   public void pushWorkInProgressChangeWhenNotOwner() throws Exception {
     TestRepository<?> userRepo = cloneProject(project, user);
     PushOneCommit.Result r =
-        pushFactory.create(user.getIdent(), userRepo).to("refs/for/master%wip");
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master%wip");
     r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     // Admin user trying to move from WIP to ready should succeed.
@@ -851,7 +853,7 @@
     assertThat(r.getChange().change().isWorkInProgress()).isTrue();
 
     // Push as change owner to move change from WIP to ready.
-    r = pushFactory.create(user.getIdent(), userRepo).to("refs/for/master%ready");
+    r = pushFactory.create(user.newIdent(), userRepo).to("refs/for/master%ready");
     r.assertOkStatus();
     assertThat(r.getChange().change().isWorkInProgress()).isFalse();
 
@@ -896,8 +898,7 @@
     assertThat(edit).isPresent();
     EditInfo editInfo = edit.get();
     r.assertMessage(
-        "Updated Changes:\n  "
-            + canonicalWebUrl.get()
+        canonicalWebUrl.get()
             + "c/"
             + project.get()
             + "/+/"
@@ -933,7 +934,7 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     // %2C is comma; the value below tests that percent decoding happens after splitting.
     // All three ways of representing space ("%20", "+", and "_" are also exercised.
     PushOneCommit.Result r = push.to("refs/for/master/%m=my_test%20+_message%2Cm=");
@@ -941,7 +942,7 @@
 
     push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1018,7 +1019,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1039,7 +1040,7 @@
 
     push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "c.txt",
@@ -1057,7 +1058,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1085,8 +1086,8 @@
     // Create a commit with different forged author and committer.
     RevCommit c =
         commitBuilder()
-            .author(user.getIdent())
-            .committer(user2.getIdent())
+            .author(user.newIdent())
+            .committer(user2.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1094,13 +1095,13 @@
     pushHead(testRepo, "refs/for/master");
 
     String changeId = GitUtil.getChangeId(testRepo, c).get();
-    assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email());
     assertThat(getReviewerEmails(changeId, ReviewerState.REVIEWER))
-        .containsExactly(user.email, user2.email);
+        .containsExactly(user.email(), user2.email());
 
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).rcpt())
-        .containsExactly(user.emailAddress, user2.emailAddress);
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress());
   }
 
   @Test
@@ -1109,23 +1110,23 @@
     // First patch set has author and committer matching change owner.
     PushOneCommit.Result r = pushTo("refs/for/master");
 
-    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
     assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty();
 
     amendBuilder()
-        .author(user.getIdent())
-        .committer(user2.getIdent())
+        .author(user.newIdent())
+        .committer(user2.newIdent())
         .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
         .create();
     pushHead(testRepo, "refs/for/master");
 
-    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+    assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
     assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER))
-        .containsExactly(user.email, user2.email);
+        .containsExactly(user.email(), user2.email());
 
     assertThat(sender.getMessages()).hasSize(1);
     assertThat(sender.getMessages().get(0).rcpt())
-        .containsExactly(user.emailAddress, user2.emailAddress);
+        .containsExactly(user.getEmailAddress(), user2.getEmailAddress());
   }
 
   /**
@@ -1143,8 +1144,8 @@
     // Create a commit with "User" as author and committer
     RevCommit c =
         commitBuilder()
-            .author(user.getIdent())
-            .committer(user.getIdent())
+            .author(user.newIdent())
+            .committer(user.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1163,11 +1164,11 @@
         get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS);
     LabelInfo cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(2);
-    int indexAdmin = admin.fullName.equals(cr.all.get(0).name) ? 0 : 1;
+    int indexAdmin = admin.fullName().equals(cr.all.get(0).name) ? 0 : 1;
     int indexUser = indexAdmin == 0 ? 1 : 0;
-    assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName);
+    assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName());
     assertThat(cr.all.get(indexAdmin).value.intValue()).isEqualTo(1);
-    assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName);
+    assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName());
     assertThat(cr.all.get(indexUser).value.intValue()).isEqualTo(0);
     assertThat(Iterables.getLast(ci.messages).message)
         .isEqualTo("Uploaded patch set 1: Code-Review+1.");
@@ -1189,8 +1190,8 @@
 
     RevCommit c =
         commitBuilder()
-            .author(admin.getIdent())
-            .committer(admin.getIdent())
+            .author(admin.newIdent())
+            .committer(admin.newIdent())
             .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
             .message(PushOneCommit.SUBJECT)
             .create();
@@ -1231,7 +1232,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1246,7 +1247,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1292,7 +1293,7 @@
     String hashtag2 = "tag2";
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1324,7 +1325,7 @@
     String hashtag4 = "tag4";
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -1341,7 +1342,7 @@
   public void pushCommitUsingSignedOffBy() throws Exception {
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1350,10 +1351,10 @@
 
     push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
-                + String.format("\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email),
+                + String.format("\n\nSigned-off-by: %s <%s>", admin.fullName(), admin.email()),
             "b.txt",
             "anotherContent");
     r = push.to("refs/for/master");
@@ -1361,7 +1362,7 @@
 
     push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertErrorStatus("not Signed-off-by author/committer/uploader in message footer");
   }
@@ -1371,13 +1372,13 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1395,7 +1396,7 @@
 
     // create a change as admin
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1406,7 +1407,7 @@
     userRepo.reset("change");
     push =
         pushFactory.create(
-            user.getIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            user.newIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1424,7 +1425,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
 
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
@@ -1443,13 +1444,13 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1464,13 +1465,13 @@
     enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     push =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     r = push.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1556,6 +1557,32 @@
   }
 
   @Test
+  public void pushWithChangeIdAboveFooter() throws Exception {
+    testPushWithChangeIdAboveFooter();
+  }
+
+  @Test
+  public void pushWithChangeIdAboveFooterWithCreateNewChangeForAllNotInTarget() throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushWithChangeIdAboveFooter();
+  }
+
+  private void testPushWithChangeIdAboveFooter() throws Exception {
+    RevCommit c =
+        createCommit(
+            testRepo,
+            PushOneCommit.SUBJECT
+                + "\n\n"
+                + "Change-Id: Ied70ea827f5bf968f1f6aaee6594e07c846d217a\n\n"
+                + "More text, uh oh.\n");
+    assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
+    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
+  }
+
+  @Test
   public void errorMessageFormat() throws Exception {
     RevCommit c = createCommit(testRepo, "Message without Change-Id");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
@@ -1578,7 +1605,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT
                 + "\n\n"
@@ -1673,7 +1700,7 @@
   @Test
   public void pushCommitWithSameChangeIdAsPredecessorChange() throws Exception {
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     RevCommit commitChange1 = r.getCommit();
@@ -1782,7 +1809,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 +1827,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 +1835,7 @@
   public void forcePushAbandonedChange() throws Exception {
     grant(project, "refs/*", Permission.PUSH, true);
     PushOneCommit push1 =
-        pushFactory.create(admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r = push1.to("refs/for/master");
     r.assertOkStatus();
 
@@ -1850,7 +1877,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();
@@ -1858,12 +1885,12 @@
 
   @Test
   public void pushWithEmailInFooter() throws Exception {
-    pushWithReviewerInFooter(user.emailAddress.toString(), user);
+    pushWithReviewerInFooter(user.getEmailAddress().toString(), user);
   }
 
   @Test
   public void pushWithNameInFooter() throws Exception {
-    pushWithReviewerInFooter(user.fullName, user);
+    pushWithReviewerInFooter(user.fullName(), user);
   }
 
   @Test
@@ -1889,7 +1916,7 @@
     r.assertOkStatus();
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -2046,7 +2073,7 @@
 
     assertThat(getPublishedComments(r.getChangeId())).isEmpty();
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     sender.clear();
     amendChange(r.getChangeId(), "refs/for/master%publish-comments");
 
@@ -2057,9 +2084,7 @@
     assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)");
 
     List<String> messages =
-        sender
-            .getMessages()
-            .stream()
+        sender.getMessages().stream()
             .map(Message::body)
             .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1))
             .collect(toList());
@@ -2162,9 +2187,9 @@
 
     assertThat(getPublishedComments(r.getChangeId())).isEmpty();
 
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
 
     r = amendChange(r.getChangeId());
     assertThat(getPublishedComments(r.getChangeId()).stream().map(c -> c.message))
@@ -2176,9 +2201,9 @@
     PushOneCommit.Result r = createChange();
     addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1"));
 
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.publishCommentsOnPush = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
 
     r = amendChange(r.getChangeId(), "refs/for/master%no-publish-comments");
 
@@ -2373,7 +2398,7 @@
     PushResult pr = pushHead(testRepo, r, false);
     assertPushOk(pr, r);
 
-    RevCommit amended = testRepo.amend(c).author(user.getIdent()).create();
+    RevCommit amended = testRepo.amend(c).author(user.newIdent()).create();
     testRepo.reset(amended);
 
     pr = pushHead(testRepo, r, false);
@@ -2410,6 +2435,74 @@
         .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
   }
 
+  @Test
+  public void sequentialCommitMessages() throws Exception {
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/";
+    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+
+    PushOneCommit.Result r1 = pushTo("refs/for/master");
+    Change.Id id1 = r1.getChange().getId();
+    r1.assertOkStatus();
+    r1.assertChange(Change.Status.NEW, null);
+    r1.assertMessage(
+        url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    PushOneCommit.Result r2 = pushTo("refs/for/master");
+    Change.Id id2 = r2.getChange().getId();
+    r2.assertOkStatus();
+    r2.assertChange(Change.Status.NEW, null);
+    r2.assertMessage(
+        url + id2 + " " + r2.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    testRepo.reset(initialHead);
+
+    // rearrange the commit so that change no. 2 is the parent of change no. 1
+    String r1Message = "Position 2";
+    String r2Message = "Position 1";
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r2Message)
+        .insertChangeId(r2.getChangeId().substring(1))
+        .create();
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r1Message)
+        .insertChangeId(r1.getChangeId().substring(1))
+        .create();
+
+    PushOneCommit.Result r3 =
+        pushFactory
+            .create(admin.newIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .to("refs/for/master");
+    Change.Id id3 = r3.getChange().getId();
+    r3.assertOkStatus();
+    r3.assertChange(Change.Status.NEW, null);
+    // should display commit r2, r1, r3 in that order.
+    r3.assertMessage(
+        "success\n"
+            + "\n"
+            + "  "
+            + url
+            + id2
+            + " "
+            + r2Message
+            + "\n"
+            + "  "
+            + url
+            + id1
+            + " "
+            + r1Message
+            + "\n"
+            + "  "
+            + url
+            + id3
+            + " another commit"
+            + NEW_CHANGE_INDICATOR
+            + "\n");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
@@ -2425,11 +2518,7 @@
   }
 
   private Collection<CommentInfo> getPublishedComments(String changeId) throws Exception {
-    return gApi.changes()
-        .id(changeId)
-        .comments()
-        .values()
-        .stream()
+    return gApi.changes().id(changeId).comments().values().stream()
         .flatMap(Collection::stream)
         .collect(toList());
   }
@@ -2444,7 +2533,7 @@
     assertThat(ci.reviewers).isNotNull();
     assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER);
     assertThat(ci.reviewers.get(ReviewerState.REVIEWER).iterator().next().email)
-        .isEqualTo(reviewer.email);
+        .isEqualTo(reviewer.email());
   }
 
   private void pushWithReviewerInFooter(String nameEmail, TestAccount expectedReviewer)
@@ -2458,9 +2547,9 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId());
+        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.id());
         // Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below.
-        gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.getId().toString()).remove();
+        gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.id().toString()).remove();
       }
       assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
     }
@@ -2471,7 +2560,7 @@
       ChangeData cd = byCommit(c);
       String name = "reviewers for " + (i + 1);
       if (expectedReviewer != null) {
-        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId());
+        assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.id());
       } else {
         assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
       }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 58051bb..d1349d0 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -26,10 +26,10 @@
 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;
-import javax.inject.Inject;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
diff --git a/javatests/com/google/gerrit/acceptance/git/ChangeRefCacheIT.java b/javatests/com/google/gerrit/acceptance/git/ChangeRefCacheIT.java
deleted file mode 100644
index 3939c67..0000000
--- a/javatests/com/google/gerrit/acceptance/git/ChangeRefCacheIT.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.git;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.util.stream.Collectors.toMap;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.git.ChangeRefCache;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
-import com.google.gerrit.server.query.change.ChangeData;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import javax.inject.Inject;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests the ChangeRefCache by running ls-remote calls and conditionally disabling the index and
- * NoteDb. The cache is enabled by default.
- *
- * <p>Why are we not just testing ChangeRefCache directly? Our ref filtering code is rather complex
- * and it is easy to get something wrong there. We want our assumptions about the performance of the
- * cache to be validated against the entire component rather than just the cache.
- */
-@NoHttpd
-public class ChangeRefCacheIT extends AbstractDaemonTest {
-
-  @Inject private ChangeRefCache changeRefCache;
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private RequestScopeOperations requestScopeOperations;
-
-  @Before
-  public void setUp() throws Exception {
-    // We want full ref evaluation so that we hit the cache every time.
-    baseConfig.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false);
-  }
-
-  /**
-   * Ensure we use only the change index for getting initial data populated and don't touch NoteDb.
-   */
-  @Test
-  public void useIndexForBootstrapping() throws Exception {
-    ChangeData change = createChange().getChange();
-    // TODO(hiesel) Rework as AutoClosable. Here and below.
-    changeRefCache.resetBootstrappedProjects();
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change.getId()),
-          change.currentPatchSet().getId().toRefName());
-    } finally {
-      enableDb(ctx);
-    }
-  }
-
-  /**
-   * Ensure we use only the change index for getting initial data populated and don't require any
-   * storage backend after that as long the data didn't change.
-   */
-  @Test
-  public void serveResultsFromCacheAfterInitialBootstrap() throws Exception {
-    ChangeData change = createChange().getChange();
-    changeRefCache.resetBootstrappedProjects();
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change.getId()),
-          change.currentPatchSet().getId().toRefName());
-    } finally {
-      enableDb(ctx);
-    }
-
-    // No change since our first call, so this time we don't bootstrap or touch NoteDb
-    AcceptanceTestRequestScope.Context ctx2 = disableDb();
-    try {
-      try (AutoCloseable ignored = disableChangeIndex()) {
-        assertUploadPackRefs(
-            "HEAD",
-            "refs/heads/master",
-            RefNames.changeMetaRef(change.getId()),
-            change.currentPatchSet().getId().toRefName());
-      }
-    } finally {
-      enableDb(ctx2);
-    }
-  }
-
-  /**
-   * Ensure we use only the change index for getting initial data populated and NoteDb for reloading
-   * data that changed since.
-   */
-  @Test
-  public void useIndexForBootstrappingAndDbForDeltaReload() throws Exception {
-    ChangeData change1 = createChange().getChange();
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    // Bootstrap: No NoteDb access as we expect it to use the index.
-    changeRefCache.resetBootstrappedProjects();
-    try {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName());
-    } finally {
-      enableDb(ctx);
-    }
-    // Delta reload: No index access as we expect it to use NoteDb.
-    ChangeData change2 = createChange().getChange();
-    try (AutoCloseable ignored = disableChangeIndex()) {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName(),
-          RefNames.changeMetaRef(change2.getId()),
-          change2.currentPatchSet().getId().toRefName());
-    }
-  }
-
-  /**
-   * Ensure we use only the change index for getting initial data populated and NoteDb for reloading
-   * data that changed since.
-   */
-  @Test
-  public void useDbForDeltaReloadOnNewPatchSet() throws Exception {
-    ChangeData change1 =
-        pushFactory
-            .create(admin.getIdent(), testRepo, "original subject", "a", "a1")
-            .to("refs/for/master")
-            .getChange();
-
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    // Bootstrap: No NoteDb access as we expect it to use the index.
-    changeRefCache.resetBootstrappedProjects();
-    try {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName());
-    } finally {
-      enableDb(ctx);
-    }
-
-    // Delta reload: No index access as we expect it to use NoteDb.
-    ChangeData change2 =
-        pushFactory
-            .create(
-                admin.getIdent(), testRepo, "subject2", "a", "a2", change1.change().getKey().get())
-            .to("refs/for/master")
-            .getChange();
-    List<PatchSet> patchSets = ImmutableList.copyOf(change2.patchSets());
-    assertThat(patchSets).hasSize(2);
-    try (AutoCloseable ctx2 = disableChangeIndex()) {
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          patchSets.get(0).getId().toRefName(),
-          patchSets.get(1).getId().toRefName());
-    }
-  }
-
-  /**
-   * Ensure we use only the change index for getting initial data populated and NoteDb for reloading
-   * data that changed since.
-   */
-  @Test
-  public void useDbForIterativeFetchingOnMetadataChange() throws Exception {
-    ChangeData change1 =
-        pushFactory
-            .create(admin.getIdent(), testRepo, "original subject", "a", "a1")
-            .to("refs/for/master")
-            .getChange();
-    // Bootstrap: No NoteDb access as we expect it to use the index.
-    AcceptanceTestRequestScope.Context ctx = disableDb();
-    try {
-      changeRefCache.resetBootstrappedProjects();
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName());
-    } finally {
-      enableDb(ctx);
-    }
-
-    try (AutoCloseable ignored = disableChangeIndex()) {
-      // user can see public change
-      requestScopeOperations.setApiUser(user.getId());
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName());
-    }
-
-    // Delta reload: No index access as we expect it to use NoteDb.
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.changes().id(change1.getId().id).setPrivate(true);
-
-    try (AutoCloseable ignored = disableChangeIndex()) {
-      // user can't see private change from admin
-      requestScopeOperations.setApiUser(user.getId());
-      assertUploadPackRefs("HEAD", "refs/heads/master");
-    }
-
-    // admin adds the user as reviewer
-    requestScopeOperations.setApiUser(admin.getId());
-    gApi.changes().id(change1.getId().id).addReviewer(user.email);
-
-    try (AutoCloseable ignored = disableChangeIndex()) {
-      // Use can see private change
-      requestScopeOperations.setApiUser(user.getId());
-      assertUploadPackRefs(
-          "HEAD",
-          "refs/heads/master",
-          RefNames.changeMetaRef(change1.getId()),
-          change1.currentPatchSet().getId().toRefName());
-    }
-  }
-
-  private void assertUploadPackRefs(String... expectedRefs) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertRefs(repo, permissionBackend.user(user(user)).project(project), expectedRefs);
-    }
-  }
-
-  private void assertRefs(
-      Repository repo, PermissionBackend.ForProject forProject, String... expectedRefs)
-      throws Exception {
-    Map<String, Ref> all = getAllRefs(repo);
-    assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
-        .containsExactlyElementsIn(expectedRefs);
-  }
-
-  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/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index 652a836..eb4845e 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -38,7 +38,7 @@
   public void forcePushNotAllowed() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     PushOneCommit push1 =
-        pushFactory.create(admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
@@ -48,7 +48,7 @@
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(admin.getIdent(), testRepo, "change2", "b.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
     PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertErrorStatus("not permitted: force update");
@@ -59,7 +59,7 @@
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     grant(project, "refs/*", Permission.PUSH, true);
     PushOneCommit push1 =
-        pushFactory.create(admin.getIdent(), testRepo, "change1", "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
@@ -69,7 +69,7 @@
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(admin.getIdent(), testRepo, "change2", "b.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
     PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
deleted file mode 100644
index 26ace25..0000000
--- a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.git;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.Sandboxed;
-import com.google.gerrit.server.AuditEvent;
-import com.google.gerrit.server.audit.HttpAuditEvent;
-import com.google.gerrit.testing.FakeGroupAuditService;
-import java.util.Collections;
-import javax.servlet.http.HttpServletResponse;
-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 {
-  private static final long AUDIT_EVENT_TIMEOUT = 500L;
-
-  @Before
-  public void beforeEach() throws Exception {
-    CredentialsProvider.setDefault(
-        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
-    selectProtocol(AbstractPushForReview.Protocol.HTTP);
-  }
-
-  @Test
-  @Sandboxed
-  public void receivePackAuditEventLog() throws Exception {
-    FakeGroupAuditService auditService = clearAuditService();
-    testRepo
-        .git()
-        .push()
-        .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
-        .call();
-    waitForAudit(auditService);
-
-    // 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();
-    assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
-  }
-
-  @Test
-  @Sandboxed
-  public void uploadPackAuditEventLog() throws Exception {
-    FakeGroupAuditService auditService = clearAuditService();
-    testRepo.git().fetch().call();
-    waitForAudit(auditService);
-
-    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");
-    assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
-  }
-
-  private FakeGroupAuditService clearAuditService() {
-    FakeGroupAuditService auditService =
-        server.getTestInjector().getInstance(FakeGroupAuditService.class);
-    auditService.clearEvents();
-    return auditService;
-  }
-
-  private void waitForAudit(FakeGroupAuditService auditService) throws InterruptedException {
-    synchronized (auditService.auditEvents) {
-      auditService.auditEvents.wait(AUDIT_EVENT_TIMEOUT);
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/javatests/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
index ed17c38..35260d0 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));
+        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 eed506c..6516b32 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -91,7 +91,7 @@
 
   private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to(ref);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 61f40f7..0ef3473 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -268,14 +268,14 @@
   @Test
   public void addPatchSetDenied() throws Exception {
     grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
     ci.branch = "master";
     ci.subject = "A change";
     Change.Id id = new Change.Id(gApi.changes().create(ci).get()._number);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ObjectId ps1Id = forceFetch(new PatchSet.Id(id, 1).toRefName());
     ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create();
     PushResult r = push(ps2Id.name() + ":refs/for/master");
@@ -344,8 +344,7 @@
   }
 
   private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
-    cfg.getAccessSections()
-        .stream()
+    cfg.getAccessSections().stream()
         .filter(
             s ->
                 s.getName().startsWith("refs/heads/")
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 79a6957..02549a7 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -24,6 +24,7 @@
 
 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.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -44,10 +45,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.server.Sequences;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.project.testing.Util;
@@ -141,25 +142,25 @@
     // visible.
     allow("refs/for/refs/heads/*", Permission.SUBMIT, admins);
     PushOneCommit.Result mr =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%submit");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%submit");
     mr.assertOkStatus();
     cd1 = mr.getChange();
     psRef1 = cd1.currentPatchSet().getId().toRefName();
     metaRef1 = RefNames.changeMetaRef(cd1.getId());
     PushOneCommit.Result br =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/branch%submit");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch%submit");
     br.assertOkStatus();
     cd2 = br.getChange();
     psRef2 = cd2.currentPatchSet().getId().toRefName();
     metaRef2 = RefNames.changeMetaRef(cd2.getId());
 
     // Second 2 changes are unmerged.
-    mr = pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master");
+    mr = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     mr.assertOkStatus();
     cd3 = mr.getChange();
     psRef3 = cd3.currentPatchSet().getId().toRefName();
     metaRef3 = RefNames.changeMetaRef(cd3.getId());
-    br = pushFactory.create(admin.getIdent(), testRepo).to("refs/for/branch");
+    br = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch");
     br.assertOkStatus();
     cd4 = br.getChange();
     psRef4 = cd4.currentPatchSet().getId().toRefName();
@@ -189,7 +190,7 @@
       u.save();
     }
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         "HEAD",
         psRef1,
@@ -233,7 +234,7 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         "HEAD", psRef1, metaRef1, psRef3, metaRef3, "refs/heads/master", "refs/tags/master-tag");
   }
@@ -243,7 +244,7 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         psRef2,
         metaRef2,
@@ -261,11 +262,11 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
 
     // Admin's edit is not visible.
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     // User's edit is visible.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
@@ -276,7 +277,6 @@
         metaRef3,
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/tags/branch-tag",
         "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
   }
 
@@ -286,14 +286,14 @@
     allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
 
     // Admin's edit on change3 is visible.
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     // Admin's edit on change4 is not visible since user cannot see the change.
     gApi.changes().id(cd4.getId().get()).edit().create();
 
     // User's edit is visible.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
@@ -304,7 +304,6 @@
         metaRef3,
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/tags/branch-tag",
         "refs/users/00/1000000/edit-" + cd3.getId() + "/1",
         "refs/users/01/1000001/edit-" + cd3.getId() + "/1");
   }
@@ -315,9 +314,9 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     assertUploadPackRefs(
         // Change 1 is visible due to accessDatabase capability, even though
@@ -352,36 +351,33 @@
   private void uploadPackNoSearchingChangeCacheImpl() throws Exception {
     allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
-    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.
-          "HEAD",
-          psRef1,
-          metaRef1,
-          psRef2,
-          metaRef2,
-          psRef3,
-          metaRef3,
-          psRef4,
-          metaRef4,
-          "refs/heads/branch",
-          "refs/heads/master",
-          "refs/tags/branch-tag",
-          "refs/tags/master-tag");
-    }
+    requestScopeOperations.setApiUser(user.id());
+    assertRefs(
+        project,
+        user,
+        // Can't use stored values from the index so DB must be enabled.
+        false,
+        "HEAD",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
   }
 
   @Test
   public void uploadPackSequencesWithAccessDatabase() throws Exception {
-    try (Repository repo = repoManager.openRepository(allProjects)) {
-      assertRefs(repo, newFilter(allProjects, user));
+    assertRefs(allProjects, user, true);
 
-      allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-      assertRefs(repo, newFilter(allProjects, user), "refs/sequences/changes");
-    }
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    assertRefs(allProjects, user, true, "refs/sequences/changes");
   }
 
   @Test
@@ -391,7 +387,7 @@
     gApi.changes().id(cd4.getId().id).delete();
     gApi.projects().name(project.get()).branch("refs/heads/branch").delete();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
         "HEAD",
         "refs/meta/config",
@@ -425,7 +421,7 @@
   public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1));
   }
@@ -451,7 +447,7 @@
 
       PersonIdent committer = serverIdent.get();
       PersonIdent author =
-          noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
+          noteUtil.newIdent(getAccount(admin.id()), committer.getWhen(), committer);
       tr.branch(RefNames.changeMetaRef(cd3.getId()))
           .commit()
           .author(author)
@@ -491,7 +487,7 @@
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
-          .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id));
+          .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id()));
     }
   }
 
@@ -502,7 +498,9 @@
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
           .containsExactly(
-              RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id), RefNames.refsUsers(admin.id));
+              RefNames.REFS_USERS_SELF,
+              RefNames.refsUsers(user.id()),
+              RefNames.refsUsers(admin.id()));
     }
   }
 
@@ -618,13 +616,13 @@
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     DraftInput draftInput = new DraftInput();
     draftInput.line = 1;
     draftInput.message = "nit: trailing whitespace";
     draftInput.path = Patch.COMMIT_MSG;
     gApi.changes().id(cd3.getId().get()).current().createDraft(draftInput);
-    String draftCommentRef = RefNames.refsDraftComments(cd3.getId(), user.id);
+    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,9 +636,9 @@
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().starChange(cd3.getId().toString());
-    String starredChangesRef = RefNames.refsStarredChanges(cd3.getId(), user.id);
+    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);
@@ -658,15 +656,15 @@
     allUsersRepo.reset("userRef");
     PushOneCommit.Result mr =
         pushFactory
-            .create(admin.getIdent(), allUsersRepo)
+            .create(admin.newIdent(), allUsersRepo)
             .to("refs/for/" + RefNames.REFS_USERS_SELF);
     mr.assertOkStatus();
 
     List<String> expectedNonMetaRefs =
         ImmutableList.of(
             RefNames.REFS_USERS_SELF,
-            RefNames.refsUsers(admin.id),
-            RefNames.refsUsers(user.id),
+            RefNames.refsUsers(admin.id()),
+            RefNames.refsUsers(user.id()),
             RefNames.REFS_EXTERNAL_IDS,
             RefNames.REFS_GROUPNAMES,
             RefNames.refsGroups(admins),
@@ -697,6 +695,22 @@
     }
   }
 
+  @Test
+  public void fetchSingleChangeWithoutIndexAccess() throws Exception {
+    PushOneCommit.Result change = createChange();
+    String patchSetRef = change.getPatchSetId().toRefName();
+    try (AutoCloseable ignored = disableChangeIndex();
+        Repository repo = repoManager.openRepository(project)) {
+      Map<String, Ref> singleRef = ImmutableMap.of(patchSetRef, repo.exactRef(patchSetRef));
+      Map<String, Ref> filteredRefs =
+          permissionBackend
+              .user(user(admin))
+              .project(project)
+              .filter(singleRef, repo, RefFilterOptions.defaults());
+      assertThat(filteredRefs).isEqualTo(singleRef);
+    }
+  }
+
   private List<String> lsRemote(Project.NameKey p, TestAccount a) throws Exception {
     TestRepository<?> testRepository = cloneProject(p, a);
     try (Git git = testRepository.git()) {
@@ -727,17 +741,23 @@
    * @throws Exception
    */
   private void assertUploadPackRefs(String... expectedRefs) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      assertRefs(repo, permissionBackend.user(user(user)).project(project), expectedRefs);
-    }
+    assertRefs(project, user, true, expectedRefs);
   }
 
   private void assertRefs(
-      Repository repo, PermissionBackend.ForProject forProject, String... expectedRefs)
+      Project.NameKey project, TestAccount user, boolean disableDb, String... expectedRefs)
       throws Exception {
-    Map<String, Ref> all = getAllRefs(repo);
-    assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
-        .containsExactlyElementsIn(expectedRefs);
+    AutoCloseable ctx = null;
+    if (disableDb) {
+      ctx = disableNoteDb();
+    }
+    try {
+      assertThat(lsRemote(project, user)).containsExactlyElementsIn(expectedRefs);
+    } finally {
+      if (disableDb) {
+        ctx.close();
+      }
+    }
   }
 
   private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs() throws Exception {
@@ -771,14 +791,12 @@
     groupInput.name = name(name);
     groupInput.ownerId = ownerGroup != null ? ownerGroup.get() : null;
     groupInput.members =
-        Arrays.stream(members).map(m -> String.valueOf(m.id.get())).collect(toList());
+        Arrays.stream(members).map(m -> String.valueOf(m.id().get())).collect(toList());
     return new AccountGroup.UUID(gApi.groups().create(groupInput).get().id);
   }
 
   private static Map<String, Ref> getAllRefs(Repository repo) throws IOException {
-    return repo.getRefDatabase()
-        .getRefs()
-        .stream()
+    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 882c0ff..759a99a 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,7 @@
 
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             PushOneCommit.SUBJECT,
             "b.txt",
@@ -230,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");
@@ -262,7 +262,7 @@
 
     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);
@@ -301,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());
@@ -324,7 +324,7 @@
     PatchSetApproval a = getSubmitter(patchSetId);
     assertThat(a.isLegacySubmit()).isTrue();
     assertThat(a.getValue()).isEqualTo((short) 1);
-    assertThat(a.getAccountId()).isEqualTo(admin.id);
+    assertThat(a.getAccountId()).isEqualTo(admin.id());
   }
 
   private void assertCommit(Project.NameKey project, String branch) throws Exception {
@@ -332,8 +332,8 @@
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
       assertThat(c.getShortMessage()).isEqualTo(PushOneCommit.SUBJECT);
-      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
-      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email());
+      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(admin.email());
     }
   }
 
@@ -343,7 +343,7 @@
       RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
       assertThat(c.getParentCount()).isEqualTo(2);
       assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
-      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email());
       assertThat(c.getCommitterIdent().getEmailAddress())
           .isEqualTo(serverIdent.get().getEmailAddress());
     }
@@ -351,7 +351,7 @@
 
   private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
       throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to(ref);
   }
 
@@ -359,7 +359,7 @@
       String ref, String subject, String fileName, String content, String changeId)
       throws Exception {
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content, changeId);
+        pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, changeId);
     return push.to(ref);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index e422c95..a04fe3c 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -495,8 +495,8 @@
       // Expect that the author name/email is preserved for the superRepo commit, but a new author
       // timestamp is used.
       PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName);
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email);
+      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
       assertThat(authorIdent.getWhen())
           .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
     } finally {
@@ -541,8 +541,8 @@
       // Expect that the author name/email is preserved for the superRepo commit, but a new author
       // timestamp is used.
       PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName);
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email);
+      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
       assertThat(authorIdent.getWhen())
           .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
       assertThat(authorIdent.getWhen())
@@ -581,7 +581,7 @@
 
       // Create change as user.
       PushOneCommit push =
-          pushFactory.create(user.getIdent(), repo2, "Change 2", "b.txt", "other content");
+          pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
       PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
       approve(pushResult2.getChangeId());
 
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 0bbe769..dc84d13 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.testing.ConfigSuite;
 import java.util.ArrayDeque;
@@ -273,7 +272,7 @@
   @Test
   public void updateManySubmodules() throws Exception {
     final int NUM = 3;
-    Project.NameKey subKey[] = new NameKey[NUM];
+    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++) {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index c166acfb..c07d512 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -76,11 +76,7 @@
           .containsExactly(adminId.get());
       // Query group index
       assertThat(
-              gApi.groups()
-                  .query("Group")
-                  .withOption(MEMBERS)
-                  .get()
-                  .stream()
+              gApi.groups().query("Group").withOption(MEMBERS).get().stream()
                   .flatMap(g -> g.members.stream())
                   .map(a -> a._accountId))
           .containsExactly(adminId.get());
@@ -271,10 +267,7 @@
 
   private void assertWriteVersions(ServerContext ctx, Integer... expected) {
     assertThat(
-            ctx.getInjector()
-                .getInstance(ChangeIndexCollection.class)
-                .getWriteIndexes()
-                .stream()
+            ctx.getInjector().getInstance(ChangeIndexCollection.class).getWriteIndexes().stream()
                 .map(i -> i.getSchema().getVersion()))
         .named("write versions")
         .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index e8cf516..e0ed78a 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -26,7 +26,6 @@
         "docker",
         "elastic",
         "exclusive",
-        "flaky",
         "pgm",
         "no_windows",
     ],
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index bb7cff7..26fe5e1 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -32,7 +32,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_5);
+    return getConfig(ElasticVersion.V6_7);
   }
 
   @ConfigSuite.Config
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 0a5510a..b30dc41 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -171,7 +171,7 @@
 
   @Test
   public void pushWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
@@ -180,7 +180,7 @@
 
   @Test
   public void pushWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
@@ -190,7 +190,7 @@
 
   @Test
   public void pushWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
@@ -200,7 +200,7 @@
 
   @Test
   public void pushForReviewWithoutTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
@@ -209,7 +209,7 @@
 
   @Test
   public void pushForReviewWithTrace() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
@@ -219,7 +219,7 @@
 
   @Test
   public void pushForReviewWithTraceAndProvidedTraceId() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 2baaef8..5e652c0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -25,9 +25,9 @@
 public class AccountAssert {
 
   public static void assertAccountInfo(TestAccount a, AccountInfo ai) {
-    assertThat(a.id.get()).isEqualTo(ai._accountId);
-    assertThat(a.fullName).isEqualTo(ai.name);
-    assertThat(a.email).isEqualTo(ai.email);
+    assertThat(a.id().get()).isEqualTo(ai._accountId);
+    assertThat(a.fullName()).isEqualTo(ai.name);
+    assertThat(a.email()).isEqualTo(ai.email);
   }
 
   public static void assertAccountInfos(List<TestAccount> expected, List<AccountInfo> actual) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/BUILD b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
index 433b854..3b46414 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/account/BUILD
@@ -18,7 +18,6 @@
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/reviewdb:server",
-        "//lib:gwtorm",
         "//lib:junit",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index 9749d67..84f218d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -43,7 +43,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.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -55,7 +55,7 @@
 public class EmailIT extends AbstractDaemonTest {
   @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;
@@ -134,11 +134,11 @@
         .get()
         .update(
             "Add External ID",
-            admin.id,
+            admin.id(),
             u ->
                 u.addExternalId(
                     ExternalId.createWithEmail(
-                        ExternalId.SCHEME_EXTERNAL, "foo", admin.id, email)));
+                        ExternalId.SCHEME_EXTERNAL, "foo", admin.id(), email)));
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
     requestScopeOperations.resetCurrentApiUser();
@@ -157,8 +157,8 @@
   @Test
   public void setPreferredEmailToEmailOfOtherAccount() throws Exception {
     exception.expect(ResourceNotFoundException.class);
-    exception.expectMessage("Not found: " + user.email);
-    gApi.accounts().self().email(user.email).setPreferred();
+    exception.expectMessage("Not found: " + user.email());
+    gApi.accounts().self().email(user.email()).setPreferred();
   }
 
   @Test
@@ -181,12 +181,12 @@
     assertThat(externalIds.get(mailtoExtIdKey)).isEmpty();
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, email));
+    Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), email));
     try {
       gApi.accounts().self().email(email).setPreferred();
       Optional<ExternalId> mailtoExtId = externalIds.get(mailtoExtIdKey);
       assertThat(mailtoExtId).isPresent();
-      assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id);
+      assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id());
       assertThat(gApi.accounts().self().get().email).isEqualTo(email);
     } finally {
       atrScope.set(oldCtx);
@@ -195,15 +195,15 @@
 
   @Test
   public void setPreferredEmailToEmailFromCustomRealmThatBelongsToOtherAccount() throws Exception {
-    ExternalId mailToExtId = ExternalId.createEmail(user.id, user.email);
+    ExternalId mailToExtId = ExternalId.createEmail(user.id(), user.email());
     assertThat(externalIds.get(mailToExtId.key())).isPresent();
 
     Context oldCtx =
-        createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, user.email));
+        createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), user.email()));
     try {
       exception.expect(ResourceConflictException.class);
       exception.expectMessage("email in use by another account");
-      gApi.accounts().self().email(user.email).setPreferred();
+      gApi.accounts().self().email(user.email()).setPreferred();
     } finally {
       atrScope.set(oldCtx);
     }
@@ -275,10 +275,10 @@
             realm,
             anonymousCowardName,
             canonicalUrl,
-            disableReverseDnsLookup,
+            enableReverseDnsLookup,
             accountCache,
             groupBackend);
-    return atrScope.set(atrScope.newContext(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 8e0aa01..8ff80a0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
@@ -54,7 +55,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -95,7 +95,7 @@
 
   @Test
   public void getExternalIds() throws Exception {
-    Collection<ExternalId> expectedIds = getAccountState(user.getId()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(user.id()).getExternalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
     RestResponse response = userRestSession.get("/accounts/self/external.ids");
@@ -111,20 +111,20 @@
 
   @Test
   public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
-    gApi.accounts().id(admin.id.get()).getExternalIds();
+    gApi.accounts().id(admin.id().get()).getExternalIds();
   }
 
   @Test
   public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
-    Collection<ExternalId> expectedIds = getAccountState(admin.getId()).getExternalIds();
+    Collection<ExternalId> expectedIds = getAccountState(admin.id()).getExternalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
-    RestResponse response = userRestSession.get("/accounts/" + admin.id + "/external.ids");
+    RestResponse response = userRestSession.get("/accounts/" + admin.id() + "/external.ids");
     response.assertOK();
 
     List<AccountExternalIdInfo> results =
@@ -137,7 +137,7 @@
 
   @Test
   public void deleteExternalIds() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
     List<String> toDelete = new ArrayList<>();
@@ -164,18 +164,18 @@
   @Test
   public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts()
-        .id(admin.id.get())
+        .id(admin.id().get())
         .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
   }
 
   @Test
   public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
     gApi.accounts()
@@ -201,11 +201,11 @@
 
     assertThat(toDelete).hasSize(1);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     RestResponse response =
-        userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete);
+        userRestSession.post("/accounts/" + admin.id() + "/external.ids:delete", toDelete);
     response.assertNoContent();
-    List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id.get()).getExternalIds();
+    List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id().get()).getExternalIds();
     // The external ID in WebSession will not be set for tests, resulting that
     // "mailto:user@example.com" can be deleted while "username:user" can't.
     assertThat(results).hasSize(1);
@@ -227,7 +227,7 @@
   @Test
   public void deleteExternalIds_Conflict() throws Exception {
     List<String> toDelete = new ArrayList<>();
-    String externalIdStr = "username:" + user.username;
+    String externalIdStr = "username:" + user.username();
     toDelete.add(externalIdStr);
     RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
     response.assertConflict();
@@ -463,7 +463,7 @@
     insertExtId(
         ExternalId.createWithPassword(
             ExternalId.Key.parse(nextId(scheme, i)),
-            admin.id,
+            admin.id(),
             "admin.other@example.com",
             "secret-password"));
     insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
@@ -563,7 +563,10 @@
 
   private ExternalId createExternalIdWithOtherCaseEmail(String externalId) {
     return ExternalId.createWithPassword(
-        ExternalId.Key.parse(externalId), admin.id, admin.email.toUpperCase(Locale.US), "password");
+        ExternalId.Key.parse(externalId),
+        admin.id(),
+        admin.email().toUpperCase(Locale.US),
+        "password");
   }
 
   private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
@@ -572,7 +575,7 @@
         repo,
         rw,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id());
           ObjectId noteId = extId.key().sha1();
           Config c = new Config();
           extId.writeToConfig(c);
@@ -590,7 +593,7 @@
         repo,
         rw,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id());
           ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
           Config c = new Config();
           extId.writeToConfig(c);
@@ -640,8 +643,8 @@
       CommitBuilder cb = new CommitBuilder();
       cb.setMessage("Update external IDs");
       cb.setTreeId(noteMap.writeTree(ins));
-      cb.setAuthor(admin.getIdent());
-      cb.setCommitter(admin.getIdent());
+      cb.setAuthor(admin.newIdent());
+      cb.setCommitter(admin.newIdent());
       if (!rev.equals(ObjectId.zeroId())) {
         cb.setParentId(rev);
       } else {
@@ -688,17 +691,18 @@
   }
 
   private ExternalId createExternalIdWithInvalidEmail(String externalId) {
-    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, "invalid-email");
+    return ExternalId.createWithEmail(
+        ExternalId.Key.parse(externalId), admin.id(), "invalid-email");
   }
 
   private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
-    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, admin.email);
+    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id(), admin.email());
   }
 
   private ExternalId createExternalIdWithBadPassword(String username) {
     return ExternalId.create(
         ExternalId.Key.create(SCHEME_USERNAME, username),
-        admin.id,
+        admin.id(),
         null,
         "non-hashed-password-is-not-allowed");
   }
@@ -723,29 +727,30 @@
 
   @Test
   public void checkNoReloadAfterUpdate() throws Exception {
-    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id));
+    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id()));
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // insert external ID
-      ExternalId extId = ExternalId.create("foo", "bar", admin.id);
+      ExternalId extId = ExternalId.create("foo", "bar", admin.id());
       insertExtId(extId);
       expectedExtIds.add(extId);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
 
       // update external ID
       expectedExtIds.remove(extId);
-      ExternalId extId2 = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com");
+      ExternalId extId2 =
+          ExternalId.createWithEmail("foo", "bar", admin.id(), "foo.bar@example.com");
       accountsUpdateProvider
           .get()
-          .update("Update External ID", admin.id, u -> u.updateExternalId(extId2));
+          .update("Update External ID", admin.id(), u -> u.updateExternalId(extId2));
       expectedExtIds.add(extId2);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
 
       // delete external ID
       accountsUpdateProvider
           .get()
-          .update("Delete External ID", admin.id, u -> u.deleteExternalId(extId));
+          .update("Delete External ID", admin.id(), u -> u.deleteExternalId(extId));
       expectedExtIds.remove(extId2);
-      assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
+      assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
     }
   }
 
@@ -753,10 +758,10 @@
   public void byAccountFailIfReadingExternalIdsFails() throws Exception {
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
       exception.expect(IOException.class);
-      externalIds.byAccount(admin.id);
+      externalIds.byAccount(admin.id());
     }
   }
 
@@ -764,28 +769,28 @@
   public void byEmailFailIfReadingExternalIdsFails() throws Exception {
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
 
       exception.expect(IOException.class);
-      externalIds.byEmail(admin.email);
+      externalIds.byEmail(admin.email());
     }
   }
 
   @Test
   public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
-    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id));
-    ExternalId newExtId = ExternalId.create("foo", "bar", admin.id);
+    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id()));
+    ExternalId newExtId = ExternalId.create("foo", "bar", admin.id());
     insertExtIdBehindGerritsBack(newExtId);
     expectedExternalIds.add(newExtId);
-    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
+    assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExternalIds);
   }
 
   @Test
   public void unsetEmail() throws Exception {
-    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id, "x@example.com");
+    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id(), "x@example.com");
     insertExtId(extId);
 
-    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id);
+    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -799,10 +804,10 @@
   @Test
   public void unsetHttpPassword() throws Exception {
     ExternalId extId =
-        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id, null, "secret");
+        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id(), null, "secret");
     insertExtId(extId);
 
-    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id);
+    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -818,22 +823,22 @@
     // Insert external ID for different accounts
     TestAccount user1 = accountCreator.create("user1");
     TestAccount user2 = accountCreator.create("user2");
-    ExternalId extId1 = ExternalId.create("foo", "1", user1.id);
-    ExternalId extId2 = ExternalId.create("foo", "2", user1.id);
-    ExternalId extId3 = ExternalId.create("foo", "3", user2.id);
+    ExternalId extId1 = ExternalId.create("foo", "1", user1.id());
+    ExternalId extId2 = ExternalId.create("foo", "2", user1.id());
+    ExternalId extId3 = ExternalId.create("foo", "3", user2.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.insert(ImmutableSet.of(extId1, extId2, extId3));
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Account: " + user2.getId())
+          .containsExactly("Account: " + user1.id(), "Account: " + user2.id())
           .inOrder();
     }
 
     // Insert external ID with different emails
-    ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id, "foo4@example.com");
-    ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id, "foo5@example.com");
+    ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id(), "foo4@example.com");
+    ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id(), "foo5@example.com");
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -841,22 +846,22 @@
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
           .containsExactly(
-              "Account: " + user1.getId(),
-              "Account: " + user2.getId(),
+              "Account: " + user1.id(),
+              "Account: " + user2.id(),
               "Email: foo4@example.com",
               "Email: foo5@example.com")
           .inOrder();
     }
 
     // Update external ID - Add Email
-    ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id, "foo1@example.com");
+    ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id(), "foo1@example.com");
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.upsert(extId1a);
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo1@example.com")
           .inOrder();
     }
 
@@ -867,7 +872,7 @@
       extIdNotes.upsert(extId1);
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo1@example.com")
           .inOrder();
     }
 
@@ -879,7 +884,7 @@
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
           .containsExactly(
-              "Account: " + user1.getId(), "Account: " + user2.getId(), "Email: foo5@example.com")
+              "Account: " + user1.id(), "Account: " + user2.id(), "Email: foo5@example.com")
           .inOrder();
     }
 
@@ -889,7 +894,7 @@
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       extIdNotes.delete(extId2.accountId(), extId2.key());
       RevCommit c = extIdNotes.commit(md);
-      assertThat(getFooters(c)).containsExactly("Account: " + user1.getId()).inOrder();
+      assertThat(getFooters(c)).containsExactly("Account: " + user1.id()).inOrder();
     }
 
     // Delete external ID by key with email
@@ -899,7 +904,7 @@
       extIdNotes.delete(extId4.accountId(), extId4.key());
       RevCommit c = extIdNotes.commit(md);
       assertThat(getFooters(c))
-          .containsExactly("Account: " + user1.getId(), "Email: foo4@example.com")
+          .containsExactly("Account: " + user1.id(), "Email: foo4@example.com")
           .inOrder();
     }
   }
@@ -928,21 +933,21 @@
       extIdNotes.insert(extId);
       try (MetaDataUpdate metaDataUpdate =
           new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
-        metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
-        metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
+        metaDataUpdate.getCommitBuilder().setAuthor(admin.newIdent());
+        metaDataUpdate.getCommitBuilder().setCommitter(admin.newIdent());
         extIdNotes.commit(metaDataUpdate);
       }
     }
   }
 
   private void addExtId(TestRepository<?> testRepo, ExternalId... extIds)
-      throws IOException, OrmDuplicateKeyException, ConfigInvalidException {
+      throws IOException, DuplicateKeyException, ConfigInvalidException {
     ExternalIdNotes extIdNotes = externalIdNotesFactory.load(testRepo.getRepository());
     extIdNotes.insert(Arrays.asList(extIds));
     try (MetaDataUpdate metaDataUpdate =
         new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, testRepo.getRepository())) {
-      metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
-      metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
+      metaDataUpdate.getCommitBuilder().setAuthor(admin.newIdent());
+      metaDataUpdate.getCommitBuilder().setCommitter(admin.newIdent());
       extIdNotes.commit(metaDataUpdate);
       extIdNotes.updateCaches();
     }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index 07bc394..27ae8b12 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -26,10 +26,10 @@
 public class GetAccountDetailIT extends AbstractDaemonTest {
   @Test
   public void getDetail() throws Exception {
-    RestResponse r = adminRestSession.get("/accounts/" + admin.username + "/detail/");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.username() + "/detail/");
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
-    Account account = getAccount(admin.getId());
+    Account account = getAccount(admin.id());
     assertThat(info.registeredOn).isEqualTo(account.getRegisteredOn());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index ed7abd2..11f7c0f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -32,19 +32,19 @@
   @Test
   public void getAccount() throws Exception {
     // by formatted string
-    testGetAccount(admin.fullName + " <" + admin.email + ">", admin);
+    testGetAccount(admin.fullName() + " <" + admin.email() + ">", admin);
 
     // by email
-    testGetAccount(admin.email, admin);
+    testGetAccount(admin.email(), admin);
 
     // by full name
-    testGetAccount(admin.fullName, admin);
+    testGetAccount(admin.fullName(), admin);
 
     // by account ID
-    testGetAccount(Integer.toString(admin.id.get()), admin);
+    testGetAccount(Integer.toString(admin.id().get()), admin);
 
     // by user name
-    testGetAccount(admin.username, admin);
+    testGetAccount(admin.username(), admin);
 
     // by 'self'
     testGetAccount("self", admin);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 2ae706a..4dec505 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -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;
@@ -86,7 +85,7 @@
     admin2 = accountCreator.admin2();
     GroupInput gi = new GroupInput();
     gi.name = name("New-Group");
-    gi.members = ImmutableList.of(user.id.toString());
+    gi.members = ImmutableList.of(user.id().toString());
     newGroup = gApi.groups().create(gi).get();
   }
 
@@ -102,22 +101,22 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = ReviewInput.recommend();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
     revision.review(in);
 
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
     assertThat(psa.getPatchSetId().get()).isEqualTo(1);
     assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id);
+    assertThat(psa.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
     ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
     assertThat(m.getMessage()).endsWith(in.message);
-    assertThat(m.getAuthor()).isEqualTo(user.id);
-    assertThat(m.getRealAuthor()).isEqualTo(admin.id);
+    assertThat(m.getAuthor()).isEqualTo(user.id());
+    assertThat(m.getRealAuthor()).isEqualTo(admin.id());
   }
 
   @Test
@@ -127,7 +126,7 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
 
     exception.expect(AuthException.class);
@@ -142,7 +141,7 @@
 
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Not-A-Label", 5);
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("label \"Not-A-Label\" is not a configured label");
@@ -155,7 +154,7 @@
 
     String changeId = createChange().getChangeId();
     ReviewInput in = new ReviewInput().label("Code-Review", 1).label("Not-A-Label", 5);
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     gApi.changes().id(changeId).current().review(in);
 
     assertThat(gApi.changes().id(changeId).get().labels).doesNotContainKey("Not-A-Label");
@@ -173,7 +172,7 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Verified", 1);
 
     exception.expect(AuthException.class);
@@ -197,7 +196,7 @@
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     CommentInput ci = new CommentInput();
     ci.path = Patch.COMMIT_MSG;
@@ -210,15 +209,15 @@
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
     assertThat(psa.getPatchSetId().get()).isEqualTo(1);
     assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id);
+    assertThat(psa.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id());
 
     ChangeData cd = r.getChange();
     Comment c = Iterables.getOnlyElement(commentsUtil.publishedByChange(cd.notes()));
     assertThat(c.message).isEqualTo(ci.message);
-    assertThat(c.author.getId()).isEqualTo(user.id);
-    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+    assertThat(c.author.getId()).isEqualTo(user.id());
+    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -227,7 +226,7 @@
     PushOneCommit.Result r = createChange();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     RobotCommentInput ci = new RobotCommentInput();
     ci.robotId = "my-robot";
@@ -244,8 +243,8 @@
     assertThat(c.message).isEqualTo(ci.message);
     assertThat(c.robotId).isEqualTo(ci.robotId);
     assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
-    assertThat(c.author.getId()).isEqualTo(user.id);
-    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+    assertThat(c.author.getId()).isEqualTo(user.id());
+    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -253,7 +252,7 @@
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
@@ -261,9 +260,9 @@
     di.message = "message";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
     in.drafts = DraftHandling.PUBLISH;
 
@@ -297,11 +296,11 @@
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
 
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id + " cannot see change");
+    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
     revision.review(in);
   }
 
@@ -309,14 +308,14 @@
   @Test
   public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowCodeReviewOnBehalfOf();
-    requestScopeOperations.setApiUser(accountCreator.user2().getId());
-    assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
+    assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
 
     PushOneCommit.Result r = createChange();
     RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
 
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.label("Code-Review", 1);
 
     exception.expect(UnprocessableEntityException.class);
@@ -332,15 +331,15 @@
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = admin2.email;
+    in.onBehalfOf = admin2.email();
     gApi.changes().id(changeId).current().submit(in);
 
     ChangeData cd = r.getChange();
-    assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(cd.change().isMerged()).isTrue();
     PatchSetApproval submitter =
         approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
-    assertThat(submitter.getAccountId()).isEqualTo(admin2.id);
-    assertThat(submitter.getRealAccountId()).isEqualTo(admin.id);
+    assertThat(submitter.getAccountId()).isEqualTo(admin2.id());
+    assertThat(submitter.getRealAccountId()).isEqualTo(admin.id());
   }
 
   @Test
@@ -365,7 +364,7 @@
         .current()
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = admin2.email;
+    in.onBehalfOf = admin2.email();
     exception.expect(AuthException.class);
     exception.expectMessage("submit on behalf of other users not permitted");
     gApi.changes().id(project.get() + "~master~" + r.getChangeId()).current().submit(in);
@@ -380,9 +379,9 @@
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = user.email;
+    in.onBehalfOf = user.email();
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id + " cannot see change");
+    exception.expectMessage("on_behalf_of account " + user.id() + " cannot see change");
     gApi.changes().id(changeId).current().submit(in);
   }
 
@@ -390,14 +389,14 @@
   @Test
   public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowSubmitOnBehalfOf();
-    requestScopeOperations.setApiUser(accountCreator.user2().getId());
-    assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
+    assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
 
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
-    in.onBehalfOf = user.email;
+    in.onBehalfOf = user.email();
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage("not found");
     exception.expectMessage(in.onBehalfOf);
@@ -407,17 +406,17 @@
   @Test
   public void runAsValidUser() throws Exception {
     allowRunAs();
-    RestResponse res = adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id()));
     res.assertOK();
     AccountInfo account = newGson().fromJson(res.getEntityContent(), AccountInfo.class);
-    assertThat(account._accountId).isEqualTo(user.id.get());
+    assertThat(account._accountId).isEqualTo(user.id().get());
   }
 
   @GerritConfig(name = "auth.enableRunAs", value = "false")
   @Test
   public void runAsDisabledByConfig() throws Exception {
     allowRunAs();
-    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent())
         .isEqualTo("X-Gerrit-RunAs disabled by auth.enableRunAs = false");
@@ -425,7 +424,7 @@
 
   @Test
   public void runAsNotPermitted() throws Exception {
-    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = adminRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
   }
@@ -433,7 +432,7 @@
   @Test
   public void runAsNeverPermittedForAnonymousUsers() throws Exception {
     allowRunAs();
-    RestResponse res = anonRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+    RestResponse res = anonRestSession.getWithHeader("/changes/", runAsHeader(user.id()));
     res.assertForbidden();
     assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
   }
@@ -451,14 +450,14 @@
     allowRunAs();
     PushOneCommit.Result r = createChange();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
     di.line = 1;
     di.message = "inline comment";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
 
     // Things that aren't allowed with on_behalf_of:
     //  - no labels.
@@ -468,19 +467,21 @@
     in.drafts = DraftHandling.PUBLISH;
     RestResponse res =
         adminRestSession.postWithHeader(
-            "/changes/" + r.getChangeId() + "/revisions/current/review", runAsHeader(user.id), in);
+            "/changes/" + r.getChangeId() + "/revisions/current/review",
+            runAsHeader(user.id()),
+            in);
     res.assertOK();
 
     ChangeMessageInfo m = Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages);
     assertThat(m.message).endsWith(in.message);
-    assertThat(m.author._accountId).isEqualTo(user.id.get());
+    assertThat(m.author._accountId).isEqualTo(user.id().get());
 
     CommentInfo c =
         Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).comments().get(di.path));
-    assertThat(c.author._accountId).isEqualTo(user.id.get());
+    assertThat(c.author._accountId).isEqualTo(user.id().get());
     assertThat(c.message).isEqualTo(di.message);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
   }
 
@@ -496,30 +497,30 @@
 
     PushOneCommit.Result r = createChange();
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
 
     String endpoint = "/changes/" + r.getChangeId() + "/revisions/current/review";
-    RestResponse res = adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id), in);
+    RestResponse res = adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id()), in);
     res.assertForbidden();
     assertThat(res.getEntityContent())
         .isEqualTo("label required to post review on behalf of \"" + in.onBehalfOf + '"');
 
     in.label("Code-Review", 1);
-    adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id), in).assertOK();
+    adminRestSession.postWithHeader(endpoint, runAsHeader(user2.id()), in).assertOK();
 
     PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
     assertThat(psa.getPatchSetId().get()).isEqualTo(1);
     assertThat(psa.getLabel()).isEqualTo("Code-Review");
-    assertThat(psa.getAccountId()).isEqualTo(user.id);
+    assertThat(psa.getAccountId()).isEqualTo(user.id());
     assertThat(psa.getValue()).isEqualTo(1);
-    assertThat(psa.getRealAccountId()).isEqualTo(admin.id); // not user2
+    assertThat(psa.getRealAccountId()).isEqualTo(admin.id()); // not user2
 
     ChangeData cd = r.getChange();
     ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
     assertThat(m.getMessage()).endsWith(in.message);
-    assertThat(m.getAuthor()).isEqualTo(user.id);
-    assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2
+    assertThat(m.getAuthor()).isEqualTo(user.id());
+    assertThat(m.getRealAuthor()).isEqualTo(admin.id()); // not user2
   }
 
   @Test
@@ -528,11 +529,11 @@
 
     PushOneCommit.Result r = createChange();
     ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
+    in.onBehalfOf = user.id().toString();
     in.message = "Message on behalf of";
     in.label("Code-Review", 1);
 
-    requestScopeOperations.setApiUser(accountCreator.user2().getId());
+    requestScopeOperations.setApiUser(accountCreator.user2().id());
     gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
@@ -540,7 +541,8 @@
 
     ChangeMessageInfo changeMessageInfo = Iterables.getLast(info.messages);
     assertThat(changeMessageInfo.realAuthor).isNotNull();
-    assertThat(changeMessageInfo.realAuthor._accountId).isEqualTo(accountCreator.user2().id.get());
+    assertThat(changeMessageInfo.realAuthor._accountId)
+        .isEqualTo(accountCreator.user2().id().get());
   }
 
   private void allowCodeReviewOnBehalfOf() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index ea71281..e05d0db 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -27,7 +27,7 @@
     UsernameInput in = new UsernameInput();
     in.username = "myUsername";
     RestResponse r =
-        adminRestSession.put("/accounts/" + accountCreator.create().id.get() + "/username", in);
+        adminRestSession.put("/accounts/" + accountCreator.create().id().get() + "/username", in);
     r.assertOK();
     assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(in.username);
   }
@@ -35,9 +35,9 @@
   @Test
   public void setExisting_Conflict() throws Exception {
     UsernameInput in = new UsernameInput();
-    in.username = admin.username;
+    in.username = admin.username();
     adminRestSession
-        .put("/accounts/" + accountCreator.create().id.get() + "/username", in)
+        .put("/accounts/" + accountCreator.create().id().get() + "/username", in)
         .assertConflict();
   }
 
@@ -45,11 +45,13 @@
   public void setNew_MethodNotAllowed() throws Exception {
     UsernameInput in = new UsernameInput();
     in.username = "newUsername";
-    adminRestSession.put("/accounts/" + admin.username + "/username", in).assertMethodNotAllowed();
+    adminRestSession
+        .put("/accounts/" + admin.username() + "/username", in)
+        .assertMethodNotAllowed();
   }
 
   @Test
   public void delete_MethodNotAllowed() throws Exception {
-    adminRestSession.put("/accounts/" + admin.username + "/username").assertMethodNotAllowed();
+    adminRestSession.put("/accounts/" + admin.username() + "/username").assertMethodNotAllowed();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index 3a68323..d0c1fa4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -58,7 +58,7 @@
 
     List<ProjectWatchInfo> persistedWatchedProjects =
         gApi.accounts().self().setWatchedProjects(projectsToWatch);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch).inOrder();
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch).inOrder();
   }
 
   @Test
@@ -92,7 +92,7 @@
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
 
     assertThat(persistedWatchedProjects).doesNotContain(pwi);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
@@ -131,7 +131,7 @@
 
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
@@ -156,7 +156,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -173,7 +173,7 @@
     gApi.accounts().self().deleteWatchedProjects(d);
 
     // Check that trying to delete a non-existing watch doesn't fail
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().deleteWatchedProjects(d);
   }
 
@@ -182,7 +182,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -205,7 +205,7 @@
 
     List<ProjectWatchInfo> watchedProjects = gApi.accounts().self().getWatchedProjects();
 
-    assertThat(watchedProjects).containsAllIn(projectsToWatch);
+    assertThat(watchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
@@ -239,11 +239,11 @@
     List<ProjectWatchInfo> persistedWatchedProjects = gApi.accounts().self().getWatchedProjects();
 
     assertThat(persistedWatchedProjects).doesNotContain(pwi);
-    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+    assertThat(persistedWatchedProjects).containsAtLeastElementsIn(projectsToWatch);
   }
 
   @Test
   public void postWithoutBody() throws Exception {
-    adminRestSession.post("/accounts/" + admin.username + "/watched.projects").assertOK();
+    adminRestSession.post("/accounts/" + admin.username() + "/watched.projects").assertOK();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
index dd6e1a5..8e5eaa4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -152,7 +152,7 @@
 
   @Test
   public void emailEndpoints() throws Exception {
-    execute(adminRestSession, EMAIL_ENDPOINTS, "self", admin.email);
+    execute(adminRestSession, EMAIL_ENDPOINTS, "self", admin.email());
   }
 
   @Test
@@ -166,12 +166,12 @@
         .get()
         .update(
             "Add Email",
-            admin.getId(),
+            admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, admin.getId(), email)));
+                    ExternalId.createWithEmail(name("test"), email, admin.id(), email)));
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.accounts()
         .self()
         .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored()), ImmutableList.of());
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index db5dfab..55744cc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -280,7 +280,7 @@
     String changeId = createChange().getChangeId();
 
     AddReviewerInput addReviewerInput = new AddReviewerInput();
-    addReviewerInput.reviewer = user.email;
+    addReviewerInput.reviewer = user.email();
 
     RestApiCallHelper.execute(
         adminRestSession,
@@ -299,7 +299,7 @@
         VOTE_ENDPOINTS,
         () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
         changeId,
-        admin.email,
+        admin.email(),
         "Code-Review");
   }
 
@@ -314,7 +314,7 @@
     String changeId = createChange().getChangeId();
 
     AddReviewerInput addReviewerInput = new AddReviewerInput();
-    addReviewerInput.reviewer = user.email;
+    addReviewerInput.reviewer = user.email();
 
     RestApiCallHelper.execute(
         adminRestSession,
@@ -335,7 +335,7 @@
         () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
         changeId,
         "current",
-        admin.email,
+        admin.email(),
         "Code-Review");
   }
 
@@ -488,8 +488,7 @@
 
   private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
     assertThatList(robotComments).isNotNull();
-    return robotComments
-        .stream()
+    return robotComments.stream()
         .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
         .filter(Objects::nonNull)
         .flatMap(List::stream)
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index f187094..02c44ef 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -101,8 +101,7 @@
     r.consume();
 
     Optional<String> id =
-        result
-            .stream()
+        result.stream()
             .filter(t -> "Log File Compressor".equals(t.command))
             .map(t -> t.id)
             .findFirst();
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
index b37bb01..bb12172 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
@@ -88,8 +88,8 @@
   @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);
+    gApi.groups().id(group).addMembers(admin.email());
+    RestApiCallHelper.execute(adminRestSession, MEMBER_ENDPOINTS, group, admin.email());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
new file mode 100644
index 0000000..27df565
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.binding;
+
+import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings nested under a core collection.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedChildRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> TEST_CALLS =
+      ImmutableSet.of(
+          // Calls that have the plugin name as part of the collection name
+          RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
+          RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/update"),
+          // Same tests but without the plugin name as part of the collection name. This works as
+          // long as there is no core collection with the same name (which takes precedence) and no
+          // other plugin binds a collection with the same name. We highly encourage plugin authors
+          // to use the fully qualified collection name instead.
+          RestCall.get("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.get("/changes/%s/revisions/%s/test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/1/update"));
+
+  /**
+   * Module for all sys bindings.
+   *
+   * <p>TODO: This should actually just move into MyPluginHttpModule. However, that doesn't work
+   * currently. This TODO is for fixing this bug.
+   */
+  static class MyPluginSysModule extends AbstractModule {
+    @Override
+    public void configure() {
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+              child(REVISION_KIND, "test-collection").to(TestChildCollection.class);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestChildCollection
+      implements ChildCollection<RevisionResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestChildCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<RevisionResource> list() throws RestApiException {
+      return (RestReadView<RevisionResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(RevisionResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<RevisionResource, TestPluginResource, String> {
+    @Override
+    public Object apply(RevisionResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() throws Exception {
+    PatchSet.Id patchSetId = createChange().getPatchSetId();
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, MyPluginSysModule.class, null, null)) {
+      RestApiCallHelper.execute(
+          adminRestSession,
+          TEST_CALLS.asList(),
+          String.valueOf(patchSetId.changeId.id),
+          String.valueOf(patchSetId.patchSetId));
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
new file mode 100644
index 0000000..178a326
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.binding;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.rest.util.RestCall.Method;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.servlet.ServletModule;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings directly under {@code /}.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedRootRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> TEST_CALLS =
+      ImmutableSet.of(
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/1/detail"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/1/update"),
+          RestCall.builder(Method.GET, "/plugins/" + PLUGIN_NAME + "/not-found")
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build());
+
+  /** Module for all HTTP bindings. */
+  static class MyPluginHttpModule extends ServletModule {
+    @Override
+    public void configureServlets() {
+      bind(TestRootCollection.class);
+
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+
+      serveRegex("/(?:a/)?test-collection/(.*)$").with(TestRestApiServlet.class);
+    }
+  }
+
+  @Singleton
+  static class TestRestApiServlet extends RestApiServlet {
+    private static final long serialVersionUID = 1L;
+
+    @Inject
+    TestRestApiServlet(RestApiServlet.Globals globals, Provider<TestRootCollection> collection) {
+      super(globals, collection);
+    }
+
+    @Override
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws ServletException, IOException {
+      // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
+      // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
+      // requests, it should be:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = "/checkers/"
+      //   pathInfo = checkerUuid
+      // Instead it does:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = ""
+      //   pathInfo = "/checkers/" + checkerUuid
+      // This results in RestApiServlet splitting the pathInfo into ["", "checkers", checkerUuid],
+      // and it passes the "" to CheckersCollection#parse, which understandably, but unfortunately,
+      // fails.
+      //
+      // This frankly seems like a bug that should be fixed, but it would quite likely break
+      // existing plugins in confusing ways. So, we work around it by introducing our own request
+      // wrapper with the correct paths.
+      HttpServletRequest req = (HttpServletRequest) servletRequest;
+      String pathInfo = req.getPathInfo();
+      String correctServletPath = "/test-collection/";
+      String fixedPathInfo = pathInfo.substring(correctServletPath.length());
+      HttpServletRequestWrapper wrapped =
+          new HttpServletRequestWrapper(req) {
+            @Override
+            public String getServletPath() {
+              return correctServletPath;
+            }
+
+            @Override
+            public String getPathInfo() {
+              return fixedPathInfo;
+            }
+          };
+
+      super.service(wrapped, (HttpServletResponse) servletResponse);
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestRootCollection implements ChildCollection<TopLevelResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestRootCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<TopLevelResource> list() throws RestApiException {
+      return (RestReadView<TopLevelResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(TopLevelResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<TopLevelResource, TestPluginResource, String> {
+    @Override
+    public Object apply(TopLevelResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() throws Exception {
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, null, MyPluginHttpModule.class, null)) {
+      RestApiCallHelper.execute(adminRestSession, TEST_CALLS.asList());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
similarity index 94%
rename from javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
index 5616ebc..d60148e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
@@ -27,12 +27,12 @@
 import org.junit.Test;
 
 /**
- * Tests for checking the bindings of the plugins REST API.
+ * 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 PluginsRestApiBindingsIT extends AbstractDaemonTest {
+public class PluginsRemoteAdminRestApiBindingsIT extends AbstractDaemonTest {
   /**
    * Plugin REST endpoints to be tested, each URL contains a placeholder for the plugin identifier.
    */
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 68622e2..f191e08 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -82,7 +82,6 @@
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -313,7 +312,7 @@
     block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
@@ -333,16 +332,16 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
 
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     submit(result.getChangeId());
   }
 
@@ -359,17 +358,17 @@
     }
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    assertThat(change.owner._accountId).isEqualTo(admin.id().get());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     submit(result.getChangeId());
   }
 
@@ -483,7 +482,7 @@
     assertThat(log).hasSize(expectedCommitCount);
 
     assertThat(commitsInRepo)
-        .containsAllOf("Initial empty repository", "Change 1", "Change 2", "Change 3");
+        .containsAtLeast("Initial empty repository", "Change 1", "Change 2", "Change 3");
     if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
       assertThat(commitsInRepo).contains("Merge changes from topic \"" + expectedTopic + "\"");
     }
@@ -580,12 +579,12 @@
     }
 
     PushOneCommit push1 =
-        pushFactory.create(admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+        pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result c1 = push1.to("refs/heads/topic");
     c1.assertOkStatus();
     PushOneCommit push2 =
         pushFactory.create(
-            admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+            admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
     PushOneCommit.Result c2 = push2.to("refs/heads/topic");
     c2.assertOkStatus();
 
@@ -609,10 +608,10 @@
     //
     RevCommit master = getRemoteHead(project, "master");
     PushOneCommit stableTip =
-        pushFactory.create(admin.getIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
     PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
     PushOneCommit mergeCommit =
-        pushFactory.create(admin.getIdent(), testRepo, "The merge commit", "merge.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
     mergeCommit.setParents(ImmutableList.of(master, stable.getCommit()));
     PushOneCommit.Result mergeReview = mergeCommit.to("refs/for/master");
     approve(mergeReview.getChangeId());
@@ -639,11 +638,11 @@
     // push directly to stable to S1
     PushOneCommit.Result s1 =
         pushFactory
-            .create(admin.getIdent(), testRepo, "new commit into stable", "stable1.txt", "")
+            .create(admin.newIdent(), testRepo, "new commit into stable", "stable1.txt", "")
             .to("refs/heads/stable");
     // move the stable tip ahead to S2
     pushFactory
-        .create(admin.getIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
+        .create(admin.newIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
         .to("refs/heads/stable");
 
     testRepo.reset(initial);
@@ -651,12 +650,12 @@
     // move the master ahead
     PushOneCommit.Result m =
         pushFactory
-            .create(admin.getIdent(), testRepo, "Move master ahead", "master.txt", "")
+            .create(admin.newIdent(), testRepo, "Move master ahead", "master.txt", "")
             .to("refs/heads/master");
 
     // create merge change
     PushOneCommit mc =
-        pushFactory.create(admin.getIdent(), testRepo, "The merge commit", "merge.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
     mc.setParents(ImmutableList.of(m.getCommit(), s1.getCommit()));
     PushOneCommit.Result mergeReview = mc.to("refs/for/master");
     approve(mergeReview.getChangeId());
@@ -826,7 +825,7 @@
     // Create a stable branch and bootstrap it.
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
     PushOneCommit push =
-        pushFactory.create(user.getIdent(), testRepo, "initial commit", "a.txt", "a");
+        pushFactory.create(user.newIdent(), testRepo, "initial commit", "a.txt", "a");
     PushOneCommit.Result change = push.to("refs/heads/stable");
 
     RevCommit stable = getRemoteHead(project, "stable");
@@ -1080,7 +1079,7 @@
     assertThat(projectOperations.project(project).hasHead("master")).isFalse();
     PushOneCommit.Result change =
         pushFactory
-            .create(admin.getIdent(), testRepo, "Change 1", ImmutableMap.of())
+            .create(admin.newIdent(), testRepo, "Change 1", ImmutableMap.of())
             .to("refs/for/master");
     change.assertOkStatus();
     // TODO(dborowitz): Use EMPTY_TREE_ID after upgrading to https://git.eclipse.org/r/127473
@@ -1099,12 +1098,12 @@
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
-          batchUpdateFactory.create(project, userFactory.create(admin.id), TimeUtil.nowTs())) {
+          batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
         bu.addOp(
             change.getChange().getId(),
             new BatchUpdateOp() {
               @Override
-              public boolean updateChange(ChangeContext ctx) throws OrmException {
+              public boolean updateChange(ChangeContext ctx) {
                 ctx.getChange().setStatus(Change.Status.NEW);
                 ctx.getUpdate(ctx.getChange().currentPatchSetId()).setStatus(Change.Status.NEW);
                 return true;
@@ -1233,7 +1232,7 @@
     LabelInfo cr = c.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).value).isEqualTo(2);
-    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(user.getId());
+    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(user.id());
   }
 
   protected void assertMerged(String changeId) throws RestApiException {
@@ -1264,7 +1263,7 @@
         approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId));
     assertThat(submitter).isNotNull();
     assertThat(submitter.isLegacySubmit()).isTrue();
-    assertThat(submitter.getAccountId()).isEqualTo(user.getId());
+    assertThat(submitter.getAccountId()).isEqualTo(user.id());
   }
 
   protected void assertNoSubmitter(String changeId, int psId) throws Exception {
@@ -1348,7 +1347,7 @@
 
   protected PushOneCommit.Result createChange(
       String subject, String fileName, String content, String topic) throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master/" + name(topic));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 21d07a7..36a09fd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -97,10 +97,10 @@
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     PushOneCommit.Result change1 =
         pushFactory
-            .create(admin.getIdent(), testRepo, "Change 1", "a", "a")
+            .create(admin.newIdent(), testRepo, "Change 1", "a", "a")
             .to("refs/for/master/" + name("topic"));
 
-    PushOneCommit push2 = pushFactory.create(admin.getIdent(), testRepo, "Change 2", "b", "b");
+    PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo, "Change 2", "b", "b");
     push2.noParents();
     PushOneCommit.Result change2 = push2.to("refs/for/master/" + name("topic"));
     change2.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 31813e3..c12adfa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -70,8 +70,9 @@
     submitWithRebase(user);
   }
 
-  private void submitWithRebase(TestAccount submitter) throws Exception {
-    requestScopeOperations.setApiUser(submitter.getId());
+  protected ImmutableList<PushOneCommit.Result> submitWithRebase(TestAccount submitter)
+      throws Exception {
+    requestScopeOperations.setApiUser(submitter.id());
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
@@ -87,8 +88,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
     assertSubmitter(change2.getChangeId(), 1, submitter);
     assertSubmitter(change2.getChangeId(), 2, submitter);
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
-    assertPersonEquals(submitter.getIdent(), headAfterSecondSubmit.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(submitter.newIdent(), headAfterSecondSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(
         initialHead, headAfterFirstSubmit, headAfterFirstSubmit, headAfterSecondSubmit);
@@ -97,6 +98,7 @@
         headAfterFirstSubmit.name(),
         change2.getChangeId(),
         headAfterSecondSubmit.name());
+    return ImmutableList.of(change, change2);
   }
 
   @Test
@@ -177,7 +179,7 @@
     PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
 
     PushOneCommit change2Push =
-        pushFactory.create(admin.getIdent(), testRepo, "Merge to master", "m.txt", "");
+        pushFactory.create(admin.newIdent(), testRepo, "Merge to master", "m.txt", "");
     change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
     PushOneCommit.Result change2 = change2Push.to("refs/for/master");
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index bd31c5f..e7f3d54 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -160,19 +160,19 @@
     requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     approve(parent);
 
     requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     String changeWithSameTopic = createChangeWithTopic().getChangeId();
 
     requestScopeOperations.setApiUserAnonymous();
     String etag3 = getETag(change);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     approve(changeWithSameTopic);
 
     requestScopeOperations.setApiUserAnonymous();
@@ -197,7 +197,7 @@
     requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     approve(parent);
 
     requestScopeOperations.setApiUserAnonymous();
@@ -328,7 +328,7 @@
     }
 
     Map<String, ActionInfo> origActions = origChange.actions;
-    assertThat(origActions.keySet()).containsAllOf("followup", "abandon");
+    assertThat(origActions.keySet()).containsAtLeast("followup", "abandon");
     assertThat(origActions.get("abandon").label).isEqualTo("Abandon");
 
     Visitor v = new Visitor();
@@ -377,7 +377,7 @@
     }
 
     Map<String, ActionInfo> origActions = gApi.changes().id(id).current().actions();
-    assertThat(origActions.keySet()).containsAllOf("cherrypick", "rebase");
+    assertThat(origActions.keySet()).containsAtLeast("cherrypick", "rebase");
     assertThat(origActions.get("rebase").label).isEqualTo("Rebase");
 
     Visitor v = new Visitor();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 621a52e..2d6227b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -15,6 +15,7 @@
 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.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -29,8 +30,8 @@
 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;
@@ -64,31 +65,31 @@
   @Test
   public void addGetAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
-    assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
+    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
 
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
   }
 
   @Test
   public void setNewAssigneeWhenExists() throws Exception {
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    setAssignee(r, user.email());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void getPastAssignees() throws Exception {
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    setAssignee(r, admin.email);
+    setAssignee(r, user.email());
+    setAssignee(r, admin.email());
     List<AccountInfo> assignees = getPastAssignees(r);
     assertThat(assignees).hasSize(2);
     Iterator<AccountInfo> itr = assignees.iterator();
-    assertThat(itr.next()._accountId).isEqualTo(user.getId().get());
-    assertThat(itr.next()._accountId).isEqualTo(admin.getId().get());
+    assertThat(itr.next()._accountId).isEqualTo(user.id().get());
+    assertThat(itr.next()._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
@@ -97,25 +98,25 @@
     PushOneCommit.Result r = createChange();
     Iterable<AccountInfo> reviewers = getReviewers(r, state);
     assertThat(reviewers).isNull();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
     reviewers = getReviewers(r, state);
     assertThat(reviewers).hasSize(1);
     AccountInfo reviewer = Iterables.getFirst(reviewers, null);
-    assertThat(reviewer._accountId).isEqualTo(user.getId().get());
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void setAlreadyExistingAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email);
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    setAssignee(r, user.email());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   @Test
   public void deleteAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
-    assertThat(deleteAssignee(r)._accountId).isEqualTo(user.getId().get());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
+    assertThat(deleteAssignee(r)._accountId).isEqualTo(user.id().get());
     assertThat(getAssignee(r)).isNull();
   }
 
@@ -128,10 +129,29 @@
   @Test
   public void setAssigneeToInactiveUser() throws Exception {
     PushOneCommit.Result r = createChange();
-    gApi.accounts().id(user.getId().get()).setActive(false);
-    exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("is not active");
-    setAssignee(r, user.email);
+    gApi.accounts().id(user.id().get()).setActive(false);
+    try {
+      setAssignee(r, user.email());
+      assert_().fail("expected UnresolvableAccountException");
+    } catch (UnresolvableAccountException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "Account '"
+                  + user.email()
+                  + "' only matches inactive accounts. To use an inactive account, retry with one"
+                  + " of the following exact account IDs:\n"
+                  + user.id()
+                  + ": User <user@example.com>");
+    }
+  }
+
+  @Test
+  public void setAssigneeToInactiveUserById() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.accounts().id(user.id().get()).setActive(false);
+    setAssignee(r, user.id().toString());
+    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
   }
 
   @Test
@@ -141,24 +161,24 @@
     PushOneCommit.Result r = createChange("refs/for/refs/meta/config");
     exception.expect(AuthException.class);
     exception.expectMessage("read not permitted");
-    setAssignee(r, user.email);
+    setAssignee(r, user.email());
   }
 
   @Test
   public void setAssigneeNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted");
-    setAssignee(r, user.email);
+    setAssignee(r, user.email());
   }
 
   @Test
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
-    assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
 
   private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 3873c9d..51c5fc8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -131,6 +131,20 @@
   }
 
   @Test
+  public void listChangeMessagesSkippedEmpty() throws Exception {
+    // Change message 1: create a change.
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    // Will be a new commit with empty change message on the meta branch.
+    addOneReviewWithEmptyChangeMessage(changeId);
+    // Change Message 2: post a review with message "message 1".
+    addOneReview(changeId, "message");
+
+    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+    assertThat(messages).hasSize(2);
+  }
+
+  @Test
   public void getOneChangeMessage() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     List<ChangeMessageInfo> messages = new ArrayList<>(gApi.changes().id(changeNum).get().messages);
@@ -143,7 +157,7 @@
   @Test
   public void deleteCannotBeAppliedWithoutAdministrateServerCapability() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     try {
       deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -157,7 +171,7 @@
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
   }
 
@@ -213,26 +227,32 @@
   private int createOneChangeWithMultipleChangeMessagesInHistory() throws Exception {
     // Creates the following commit history on the meta branch of the test change.
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     // Commit 1: create a change.
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
-    // Commit 2: post a review with message "message 1".
-    requestScopeOperations.setApiUser(admin.getId());
+    // Commit 2: post an empty change message.
+    requestScopeOperations.setApiUser(admin.id());
+    addOneReviewWithEmptyChangeMessage(changeId);
+    // Commit 3: post a review with message "message 1".
     addOneReview(changeId, "message 1");
-    // Commit 3: amend a new patch set.
-    requestScopeOperations.setApiUser(user.getId());
+    // Commit 4: amend a new patch set.
+    requestScopeOperations.setApiUser(user.id());
     amendChange(changeId);
-    // Commit 4: post a review with message "message 2".
+    // Commit 5: post a review with message "message 2".
     addOneReview(changeId, "message 2");
-    // Commit 5: amend a new patch set.
+    // Commit 6: amend a new patch set.
     amendChange(changeId);
-    // Commit 6: approve the change.
-    requestScopeOperations.setApiUser(admin.getId());
+    // Commit 7: approve the change.
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
-    // commit 7: submit the change.
+    // commit 8: submit the change.
     gApi.changes().id(changeId).current().submit();
 
+    // Verifies there is only 7 change messages although there are 8 commits.
+    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+    assertThat(messages).hasSize(7);
+
     return result.getChange().getId().get();
   }
 
@@ -249,6 +269,10 @@
     gApi.changes().id(changeId).current().review(reviewInput);
   }
 
+  private void addOneReviewWithEmptyChangeMessage(String changeId) throws Exception {
+    gApi.changes().id(changeId).current().review(new ReviewInput());
+  }
+
   private void deleteOneChangeMessage(
       int changeNum, int deletedMessageIndex, TestAccount deletedBy, String reason)
       throws Exception {
@@ -262,7 +286,7 @@
     ChangeMessageInfo info = gApi.changes().id(changeNum).message(id).delete(input);
 
     // Verify the return change message info is as expect.
-    assertThat(info.message).isEqualTo(createNewChangeMessage(deletedBy.fullName, reason));
+    assertThat(info.message).isEqualTo(createNewChangeMessage(deletedBy.fullName(), reason));
     List<ChangeMessageInfo> messagesAfterDeletion = gApi.changes().id(changeNum).messages();
     assertMessagesAfterDeletion(
         messagesBeforeDeletion, messagesAfterDeletion, deletedMessageIndex, deletedBy, reason);
@@ -273,8 +297,7 @@
     assertThat(changes.stream().map(c -> c._number).collect(toSet())).contains(changeNum);
 
     // Verifies states of commits.
-    assertMetaCommitsAfterDeletion(
-        commitsBefore, changeNum, deletedMessageIndex, deletedBy, reason);
+    assertMetaCommitsAfterDeletion(commitsBefore, changeNum, id, deletedBy, reason);
   }
 
   private void assertMessagesAfterDeletion(
@@ -303,7 +326,7 @@
 
       if (i == deletedMessageIndex) {
         assertThat(after.message)
-            .isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+            .isEqualTo(createNewChangeMessage(deletedBy.fullName(), deleteReason));
       } else {
         assertThat(after.message).isEqualTo(before.message);
       }
@@ -313,7 +336,7 @@
   private void assertMetaCommitsAfterDeletion(
       List<RevCommit> commitsBeforeDeletion,
       int changeNum,
-      int deletedMessageIndex,
+      String deletedMessageId,
       TestAccount deletedBy,
       String deleteReason)
       throws Exception {
@@ -324,7 +347,7 @@
     for (int i = 0; i < commitsBeforeDeletion.size(); i++) {
       RevCommit commitBefore = commitsBeforeDeletion.get(i);
       RevCommit commitAfter = commitsAfterDeletion.get(i);
-      if (i == deletedMessageIndex) {
+      if (commitBefore.getId().getName().equals(deletedMessageId)) {
         byte[] rawBefore = commitBefore.getRawBuffer();
         byte[] rawAfter = commitAfter.getRawBuffer();
         Charset encodingBefore = RawParseUtils.parseEncoding(rawBefore);
@@ -367,7 +390,7 @@
                 rawAfter,
                 rangeAfter.get().changeMessageStart(),
                 rangeAfter.get().changeMessageEnd() + 1);
-        assertThat(message).isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+        assertThat(message).isEqualTo(createNewChangeMessage(deletedBy.fullName(), deleteReason));
       } else {
         assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage());
       }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 993c10e..d51221e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -40,7 +40,7 @@
 
   @Before
   public void setUp() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     user2 = accountCreator.user2();
   }
 
@@ -66,22 +66,22 @@
 
   @Test
   public void testChangeOwner_OwnerACLGrantedOnParentProject() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     grantApproveToChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     approve(user, createMyChange(childRepo));
   }
 
   @Test
   public void testChangeOwner_BlockedOnParentProject() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     blockApproveForChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     grantApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -95,11 +95,11 @@
 
   @Test
   public void testChangeOwner_BlockedOnParentProjectAndExclusiveAllowOnChild() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     blockApproveForChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     grantExclusiveApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -112,7 +112,7 @@
   }
 
   private void approve(TestAccount a, String changeId) throws Exception {
-    Context old = requestScopeOperations.setApiUser(a.getId());
+    Context old = requestScopeOperations.setApiUser(a.id());
     try {
       gApi.changes().id(changeId).current().review(ReviewInput.approve());
     } finally {
@@ -147,7 +147,7 @@
   }
 
   private String createMyChange(TestRepository<InMemoryRepository> testRepo) throws Exception {
-    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
     return push.to("refs/for/master").getChangeId();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 13c76e3..ac00e38 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -21,7 +21,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.Context;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -75,7 +74,7 @@
   @Test
   public void addByEmailAndById() throws Exception {
     AccountInfo byEmail = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
-    AccountInfo byId = new AccountInfo(user.id.get());
+    AccountInfo byId = new AccountInfo(user.id().get());
 
     for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
       PushOneCommit.Result r = createChange();
@@ -86,7 +85,7 @@
       gApi.changes().id(r.getChangeId()).addReviewer(inputByEmail);
 
       AddReviewerInput inputById = new AddReviewerInput();
-      inputById.reviewer = user.email;
+      inputById.reviewer = user.email();
       inputById.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(inputById);
 
@@ -198,9 +197,9 @@
       // Review change as user
       ReviewInput reviewInput = new ReviewInput();
       reviewInput.message = "I have a comment";
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       revision(r).review(reviewInput);
-      requestScopeOperations.setApiUser(admin.getId());
+      requestScopeOperations.setApiUser(admin.id());
 
       sender.clear();
 
@@ -210,7 +209,7 @@
       List<Message> messages = sender.getMessages();
       assertThat(messages).hasSize(1);
       assertThat(messages.get(0).rcpt())
-          .containsExactly(Address.parse(addInput.reviewer), user.emailAddress);
+          .containsExactly(Address.parse(addInput.reviewer), user.getEmailAddress());
       sender.clear();
     }
   }
@@ -251,7 +250,7 @@
 
     // Also add user as a regular reviewer
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.REVIEWER;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
 
@@ -307,7 +306,9 @@
         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();
   }
 
@@ -323,14 +324,11 @@
       input.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(input);
 
-      Context oldCtx = disableDb();
-      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 {
-        enableDb(oldCtx);
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index dec839c..173b78d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -80,7 +80,7 @@
     List<TestAccount> users = createAccounts(largeGroupSize, "addGroupAsReviewer");
     List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
     for (TestAccount u : users) {
-      largeGroupUsernames.add(u.username);
+      largeGroupUsernames.add(u.username());
     }
     List<String> mediumGroupUsernames = largeGroupUsernames.subList(0, mediumGroupSize);
     gApi.groups()
@@ -127,26 +127,26 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     AddReviewerResult result = addReviewer(changeId, in);
 
-    assertThat(result.input).isEqualTo(user.email);
+    assertThat(result.input).isEqualTo(user.email());
     assertThat(result.confirm).isNull();
     assertThat(result.error).isNull();
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
     assertThat(result.reviewers).isNull();
     assertThat(result.ccs).hasSize(1);
     AccountInfo ai = result.ccs.get(0);
-    assertThat(ai._accountId).isEqualTo(user.id.get());
+    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);
-    assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review.");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has uploaded this change for review.");
   }
 
   @Test
@@ -154,7 +154,7 @@
     List<TestAccount> users = createAccounts(6, "addCcGroup");
     List<String> usernames = new ArrayList<>(6);
     for (TestAccount u : users) {
-      usernames.add(u.username);
+      usernames.add(u.username());
     }
 
     List<TestAccount> firstUsers = users.subList(0, 3);
@@ -183,19 +183,19 @@
     Message m = messages.get(0);
     List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
     for (TestAccount u : firstUsers) {
-      expectedAddresses.add(u.emailAddress);
+      expectedAddresses.add(u.getEmailAddress());
     }
     assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
 
     // CC a group that overlaps with some existing reviewers and CCed accounts.
     TestAccount reviewer =
         accountCreator.create(name("reviewer"), "addCcGroup-reviewer@example.com", "Reviewer");
-    result = addReviewer(changeId, reviewer.username);
+    result = addReviewer(changeId, reviewer.username());
     assertThat(result.error).isNull();
     sender.clear();
     in.reviewer = groupOperations.newGroup().name("cc2").create().get();
     gApi.groups().id(in.reviewer).addMembers(usernames.toArray(new String[usernames.size()]));
-    gApi.groups().id(in.reviewer).addMembers(reviewer.username);
+    gApi.groups().id(in.reviewer).addMembers(reviewer.username());
     result = addReviewer(changeId, in);
     assertThat(result.input).isEqualTo(in.reviewer);
     assertThat(result.confirm).isNull();
@@ -211,9 +211,9 @@
     m = messages.get(0);
     expectedAddresses = new ArrayList<>(4);
     for (int i = 0; i < 3; i++) {
-      expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
+      expectedAddresses.add(users.get(users.size() - i - 1).getEmailAddress());
     }
-    expectedAddresses.add(reviewer.emailAddress);
+    expectedAddresses.add(reviewer.getEmailAddress());
     assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
   }
 
@@ -222,7 +222,7 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     addReviewer(changeId, in);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
@@ -263,7 +263,7 @@
     PushOneCommit.Result r = createChange();
 
     // user adds self as REVIEWER.
-    ReviewInput input = new ReviewInput().reviewer(user.username);
+    ReviewInput input = new ReviewInput().reviewer(user.username());
     RestResponse resp =
         userRestSession.post(
             "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
@@ -282,7 +282,7 @@
     assertThat(label.all).isNotNull();
     assertThat(label.all).hasSize(1);
     ApprovalInfo approval = label.all.get(0);
-    assertThat(approval._accountId).isEqualTo(user.getId().get());
+    assertThat(approval._accountId).isEqualTo(user.id().get());
   }
 
   @Test
@@ -291,7 +291,7 @@
     PushOneCommit.Result r = createChange();
 
     // user adds self as CC.
-    ReviewInput input = new ReviewInput().reviewer(user.username, CC, false);
+    ReviewInput input = new ReviewInput().reviewer(user.username(), CC, false);
     RestResponse resp =
         userRestSession.post(
             "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
@@ -326,7 +326,7 @@
     assertThat(label.all).isNull();
 
     // Add user as REVIEWER.
-    ReviewInput input = new ReviewInput().reviewer(user.username);
+    ReviewInput input = new ReviewInput().reviewer(user.username());
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNull();
     assertThat(result.reviewers).isNotNull();
@@ -345,8 +345,8 @@
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.getId().get(), 0);
-    assertThat(approvals).containsEntry(user.getId().get(), 0);
+    assertThat(approvals).containsEntry(admin.id().get(), 0);
+    assertThat(approvals).containsEntry(user.id().get(), 0);
 
     // Comment as user without voting. This should delete the approval and
     // then replace it with the default value.
@@ -370,8 +370,8 @@
     for (ApprovalInfo approval : label.all) {
       approvals.put(approval._accountId, approval.value);
     }
-    assertThat(approvals).containsEntry(admin.getId().get(), 0);
-    assertThat(approvals).containsEntry(user.getId().get(), 0);
+    assertThat(approvals).containsEntry(admin.id().get(), 0);
+    assertThat(approvals).containsEntry(user.id().get(), 0);
   }
 
   @Test
@@ -379,7 +379,7 @@
     TestAccount observer = accountCreator.user2();
     PushOneCommit.Result r = createChange();
     ReviewInput input =
-        ReviewInput.approve().reviewer(user.email).reviewer(observer.email, CC, false);
+        ReviewInput.approve().reviewer(user.email()).reviewer(observer.email(), CC, false);
 
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNotNull();
@@ -397,14 +397,14 @@
     assertThat(messages).hasSize(2);
 
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
-    assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress(), observer.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has posted comments on this change.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
     assertThat(m.body()).contains("Patch Set 1: Code-Review+2");
 
     m = messages.get(1);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
-    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress(), observer.getEmailAddress());
+    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
   }
 
@@ -415,7 +415,7 @@
     List<TestAccount> users = createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
     List<String> usernames = new ArrayList<>(largeGroupSize);
     for (TestAccount u : users) {
-      usernames.add(u.username);
+      usernames.add(u.username());
     }
 
     String largeGroup = groupOperations.newGroup().name("largeGroup").create().get();
@@ -431,8 +431,8 @@
     // Attempt to add overly large group as reviewers.
     ReviewInput input =
         ReviewInput.approve()
-            .reviewer(user.email)
-            .reviewer(observer.email, CC, false)
+            .reviewer(user.email())
+            .reviewer(observer.email(), CC, false)
             .reviewer(largeGroup);
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
     assertThat(result.labels).isNull();
@@ -454,8 +454,8 @@
     // confirmation, as reviewers.
     input =
         ReviewInput.approve()
-            .reviewer(user.email)
-            .reviewer(observer.email, CC, false)
+            .reviewer(user.email())
+            .reviewer(observer.email(), CC, false)
             .reviewer(mediumGroup);
     result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
     assertThat(result.labels).isNull();
@@ -474,7 +474,7 @@
     assertThat(c.reviewers.get(CC)).isNull();
 
     // Retrying with confirmation should successfully approve and add reviewers/CCs.
-    input = ReviewInput.approve().reviewer(user.email).reviewer(mediumGroup, CC, true);
+    input = ReviewInput.approve().reviewer(user.email()).reviewer(mediumGroup, CC, true);
     result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.labels).isNotNull();
     assertThat(result.reviewers).isNotNull();
@@ -492,7 +492,7 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     in.state = CC;
     addReviewer(changeId, in);
 
@@ -501,7 +501,7 @@
 
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     // NoteDb adds reviewer to a change on every review.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
@@ -514,28 +514,28 @@
     Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
     ReviewerUpdateInfo reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(CC);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
 
     reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(REVIEWER);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
 
     reviewerChange = it.next();
     assertThat(reviewerChange.state).isEqualTo(REMOVED);
-    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
-    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+    assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
+    assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
   }
 
   @Test
   public void addDuplicateReviewers() throws Exception {
     PushOneCommit.Result r = createChange();
-    ReviewInput input = ReviewInput.approve().reviewer(user.email).reviewer(user.email);
+    ReviewInput input = ReviewInput.approve().reviewer(user.email()).reviewer(user.email());
     ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(result.reviewers).isNotNull();
     assertThat(result.reviewers).hasSize(1);
-    AddReviewerResult reviewerResult = result.reviewers.get(user.email);
+    AddReviewerResult reviewerResult = result.reviewers.get(user.email());
     assertThat(reviewerResult).isNotNull();
     assertThat(reviewerResult.confirm).isNull();
     assertThat(reviewerResult.error).isNull();
@@ -552,8 +552,8 @@
         accountCreator.create(name("user3"), emailPrefix + "user3@example.com", "User3");
     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);
+    gApi.groups().id(group1).addMembers(user1.username(), user2.username());
+    gApi.groups().id(group2).addMembers(user2.username(), user3.username());
 
     PushOneCommit.Result r = createChange();
     ReviewInput input = ReviewInput.approve().reviewer(group1).reviewer(group2);
@@ -600,7 +600,7 @@
   public void removingReviewerRemovesTheirVote() throws Exception {
     String crLabel = "Code-Review";
     PushOneCommit.Result r = createChange();
-    ReviewInput input = ReviewInput.approve().reviewer(admin.email);
+    ReviewInput input = ReviewInput.approve().reviewer(admin.email());
     ReviewResult addResult = review(r.getChangeId(), r.getCommit().name(), input);
     assertThat(addResult.reviewers).isNotNull();
     assertThat(addResult.reviewers).hasSize(1);
@@ -615,7 +615,7 @@
     assertThat(changeLabels.get(crLabel).all).isNull();
 
     // Check that the vote is gone even after the reviewer is added back
-    addReviewer(r.getChangeId(), admin.email);
+    addReviewer(r.getChangeId(), admin.email());
     changeLabels = getChangeLabels(r.getChangeId());
     assertThat(changeLabels.get(crLabel).all).isNull();
   }
@@ -626,15 +626,15 @@
     TestAccount userToNotify = createAccounts(1, "notify-details-post-review").get(0);
 
     ReviewInput reviewInput = new ReviewInput();
-    reviewInput.reviewer(user.email, ReviewerState.REVIEWER, true);
+    reviewInput.reviewer(user.email(), ReviewerState.REVIEWER, true);
     reviewInput.notify = NotifyHandling.NONE;
     reviewInput.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
 
     sender.clear();
     gApi.changes().id(r.getChangeId()).current().review(reviewInput);
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.getEmailAddress());
   }
 
   @Test
@@ -643,15 +643,15 @@
     TestAccount userToNotify = createAccounts(1, "notify-details-post-reviewers").get(0);
 
     AddReviewerInput addReviewer = new AddReviewerInput();
-    addReviewer.reviewer = user.email;
+    addReviewer.reviewer = user.email();
     addReviewer.notify = NotifyHandling.NONE;
     addReviewer.notifyDetails =
-        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email())));
 
     sender.clear();
     gApi.changes().id(r.getChangeId()).addReviewer(addReviewer);
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.getEmailAddress());
   }
 
   @Test
@@ -659,12 +659,12 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
-    requestScopeOperations.setApiUser(newUser.getId());
+    requestScopeOperations.setApiUser(newUser.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
@@ -676,10 +676,10 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
     grant(project, RefNames.REFS + "*", Permission.REMOVE_REVIEWER, false, REGISTERED_USERS);
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     assertThatUserIsOnlyReviewer(r.getChangeId());
-    requestScopeOperations.setApiUser(newUser.getId());
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    requestScopeOperations.setApiUser(newUser.id());
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
   }
 
@@ -688,11 +688,11 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
-    requestScopeOperations.setApiUser(newUser.getId());
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    requestScopeOperations.setApiUser(newUser.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
@@ -701,13 +701,13 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
-    requestScopeOperations.setApiUser(newUser.getId());
+    requestScopeOperations.setApiUser(newUser.id());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
-    gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+    gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
   }
 
   @Test
@@ -715,13 +715,13 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.REVIEWER;
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
     assertThat(result.reviewers).hasSize(1);
     ReviewerInfo info = result.reviewers.get(0);
-    assertThat(info._accountId).isEqualTo(user.id.get());
+    assertThat(info._accountId).isEqualTo(user.id().get());
 
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).reviewers).isEmpty();
   }
@@ -731,21 +731,21 @@
     PushOneCommit.Result r = createChange();
 
     AddReviewerInput input = new AddReviewerInput();
-    input.reviewer = user.email;
+    input.reviewer = user.email();
     input.state = ReviewerState.CC;
 
     AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
     assertThat(result.ccs).hasSize(1);
     AccountInfo info = result.ccs.get(0);
-    assertThat(info._accountId).isEqualTo(user.id.get());
+    assertThat(info._accountId).isEqualTo(user.id().get());
 
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
   }
 
   private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
-    AccountInfo userInfo = new AccountInfo(user.fullName, user.emailAddress.getEmail());
-    userInfo._accountId = user.id.get();
-    userInfo.username = user.username;
+    AccountInfo userInfo = new AccountInfo(user.fullName(), user.getEmailAddress().getEmail());
+    userInfo._accountId = user.id().get();
+    userInfo.username = user.username();
     assertThat(gApi.changes().id(changeId).get().reviewers)
         .containsExactly(ReviewerState.REVIEWER, ImmutableList.of(userInfo));
   }
@@ -772,7 +772,7 @@
   }
 
   private RestResponse deleteReviewer(String changeId, TestAccount account) throws Exception {
-    return adminRestSession.delete("/changes/" + changeId + "/reviewers/" + account.getId().get());
+    return adminRestSession.delete("/changes/" + changeId + "/reviewers/" + account.id().get());
   }
 
   private ReviewResult review(String changeId, String revisionId, ReviewInput in) throws Exception {
@@ -816,7 +816,7 @@
     }
     List<Integer> expectedAccountIds = new ArrayList<>();
     for (TestAccount account : accounts) {
-      expectedAccountIds.add(account.getId().get());
+      expectedAccountIds.add(account.id().get());
     }
     assertThat(actualAccountIds).containsExactlyElementsIn(expectedAccountIds);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index b6547f0..9a907aa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -56,7 +56,7 @@
       u.save();
     }
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     fetchRefsMetaConfig();
   }
 
@@ -100,13 +100,13 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void onlyAdminMayUpdateProjectParent() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ProjectInput parent = new ProjectInput();
     parent.name = name("parent");
     parent.permissionsOnly = true;
     gApi.projects().create(parent);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     Config cfg = readProjectConfig();
     assertThat(cfg.getString("access", null, "inheritFrom")).isAnyOf(null, allProjects.get());
     cfg.setString("access", null, "inheritFrom", parent.name);
@@ -136,7 +136,7 @@
     assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
         .isAnyOf(null, allProjects.get());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(id).current().submit();
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(parent.name);
@@ -146,7 +146,7 @@
 
   @Test
   public void rejectDoubleInheritance() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     // Create separate projects to test the config
     Project.NameKey parent = createProjectOverAPI("projectToInheritFrom", null, true, null);
     Project.NameKey child = createProjectOverAPI("projectWithMalformedConfig", null, true, null);
@@ -168,7 +168,7 @@
     GitUtil.fetch(childRepo, RefNames.REFS_CONFIG + ":cfg");
     childRepo.reset("cfg");
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), childRepo, "Subject", "project.config", config);
+        pushFactory.create(admin.newIdent(), childRepo, "Subject", "project.config", config);
     PushOneCommit.Result res = push.to(RefNames.REFS_CONFIG);
     res.assertErrorStatus();
     res.assertMessage("cannot inherit from multiple projects");
@@ -194,7 +194,7 @@
     PushOneCommit.Result r =
         pushFactory
             .create(
-                user.getIdent(), testRepo, "Update project config", "project.config", cfg.toText())
+                user.newIdent(), testRepo, "Update project config", "project.config", cfg.toText())
             .to("refs/for/refs/meta/config");
     r.assertOkStatus();
     return r;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
index 6fd3bab..9c42542 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -205,7 +205,7 @@
     BasicCookieStore cookies = new BasicCookieStore();
     Executor http = Executor.newInstance().cookieStore(cookies);
 
-    Request req = Request.Get(canonicalWebUrl.get() + "/login/?account_id=" + admin.id.get());
+    Request req = Request.Get(canonicalWebUrl.get() + "/login/?account_id=" + admin.id().get());
     http.execute(req);
     String auth = null;
     for (Cookie c : cookies.getCookies()) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 08daa18..2a8293f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -48,6 +48,7 @@
 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;
@@ -153,18 +154,18 @@
 
   @Test
   public void notificationsOnChangeCreation() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     watch(project.get());
 
     // check that watcher is notified
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
-    assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review.");
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(m.body()).contains(admin.fullName() + " has uploaded this change for review.");
 
     // check that watcher is not notified if notify=NONE
     sender.clear();
@@ -183,7 +184,7 @@
       assertThat(message)
           .contains(
               String.format(
-                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
     } finally {
       setSignedOffByFooter(false);
     }
@@ -204,7 +205,7 @@
       assertThat(message)
           .contains(
               String.format(
-                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+                  "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
     } finally {
       setSignedOffByFooter(false);
     }
@@ -268,16 +269,6 @@
   }
 
   @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 {
     ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
     try (Repository repo = repoManager.openRepository(project);
@@ -288,7 +279,7 @@
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+          changeNoteUtil.newIdent(getAccount(admin.id()), c.created, serverIdent.get());
       assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
 
       assertThat(commit.getCommitterIdent())
@@ -456,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();
@@ -469,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);
@@ -491,12 +514,12 @@
 
   // TODO(davido): Expose setting of account preferences in the API
   private void setSignedOffByFooter(boolean value) throws Exception {
-    RestResponse r = adminRestSession.get("/accounts/" + admin.email + "/preferences");
+    RestResponse r = adminRestSession.get("/accounts/" + admin.email() + "/preferences");
     r.assertOK();
     GeneralPreferencesInfo i = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
     i.signedOffBy = value;
 
-    r = adminRestSession.put("/accounts/" + admin.email + "/preferences", i);
+    r = adminRestSession.put("/accounts/" + admin.email() + "/preferences", i);
     r.assertOK();
     GeneralPreferencesInfo o = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
 
@@ -540,7 +563,7 @@
     // create a initial commit in master
     Result initialCommit =
         pushFactory
-            .create(user.getIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
             .to("refs/heads/master");
     initialCommit.assertOkStatus();
 
@@ -551,13 +574,13 @@
     // create a commit in branchA
     Result changeA =
         pushFactory
-            .create(user.getIdent(), testRepo, "change A", fileA, "A content")
+            .create(user.newIdent(), testRepo, "change A", fileA, "A content")
             .to("refs/heads/" + branchA);
     changeA.assertOkStatus();
 
     // create a commit in branchB
     PushOneCommit commitB =
-        pushFactory.create(user.getIdent(), testRepo, "change B", fileB, "B content");
+        pushFactory.create(user.newIdent(), testRepo, "change B", fileB, "B content");
     commitB.setParent(initialCommit.getCommit());
     Result changeB = commitB.to("refs/heads/" + branchB);
     changeB.assertOkStatus();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 1c1c455..b0fad7e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -55,7 +55,7 @@
 
     PushOneCommit.Result r2 = amendChange(r.getChangeId());
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     recommend(r.getChangeId());
 
     sender.clear();
@@ -64,7 +64,7 @@
             + r.getChangeId()
             + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
             + "/reviewers/"
-            + user.getId().toString()
+            + user.id().toString()
             + "/votes/Code-Review";
 
     RestResponse response = adminRestSession.delete(endPoint);
@@ -73,17 +73,17 @@
     List<FakeEmailSender.Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     FakeEmailSender.Message msg = messages.get(0);
-    assertThat(msg.rcpt()).containsExactly(user.emailAddress);
-    assertThat(msg.body()).contains(admin.fullName + " has removed a vote on this change.\n");
+    assertThat(msg.rcpt()).containsExactly(user.getEmailAddress());
+    assertThat(msg.body()).contains(admin.fullName() + " has removed a vote on this change.\n");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
+        .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
 
     endPoint =
         "/changes/"
             + r.getChangeId()
             + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
             + "/reviewers/"
-            + user.getId().toString()
+            + user.id().toString()
             + "/votes";
 
     response = adminRestSession.get(endPoint);
@@ -97,10 +97,10 @@
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     ChangeMessageInfo message = Iterables.getLast(c.messages);
-    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.author._accountId).isEqualTo(admin.id().get());
     assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
     assertThat(getReviewers(c.reviewers.get(REVIEWER)))
-        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
   }
 
   private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 2b0ba4e..c13df49 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -257,7 +257,7 @@
   @Test
   public void addHashtagWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("edit hashtags not permitted");
     addHashtags(r, "MyHashtag");
@@ -267,7 +267,7 @@
   public void addHashtagWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     addHashtags(r, "MyHashtag");
     assertThatGet(r).containsExactly("MyHashtag");
     assertMessage(r, "Hashtag added: MyHashtag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index cf5136b..f49d1fb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -58,7 +58,7 @@
     TestAccount user2 = accountCreator.user2();
     AccountGroup.UUID groupId = groupOperations.newGroup().name("test").create();
     String group = groupOperations.group(groupId).get().name();
-    gApi.groups().id(group).addMembers("admin", "user", user2.username);
+    gApi.groups().id(group).addMembers("admin", "user", user2.username());
 
     // Create a project and restrict its visibility to the group
     Project.NameKey p = projectOperations.newProject().create();
@@ -74,39 +74,39 @@
 
     // Clone it and push a change as a regular user
     TestRepository<InMemoryRepository> repo = cloneProject(p, user);
-    PushOneCommit push = pushFactory.create(user.getIdent(), repo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), repo);
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
-    assertThat(result.getChange().change().getOwner()).isEqualTo(user.id);
+    assertThat(result.getChange().change().getOwner()).isEqualTo(user.id());
     String changeId = result.getChangeId();
 
     // User can see the change and it is mergeable
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     List<ChangeInfo> changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isNotNull();
 
     // Other user can see the change and it is mergeable
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
 
     // Remove the user from the group so they can no longer see the project
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.groups().id(group).removeMembers("user");
 
     // User can no longer see the change
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).isEmpty();
 
     // Reindex the change
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(changeId).index();
 
     // Other user can still see the change and it is still mergeable
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index ae6c14c..0db3508 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -48,7 +48,7 @@
     String subject = "Change subject";
     String fileName = "a.txt";
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content, baseChangeId);
+        pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, baseChangeId);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     return r;
@@ -65,7 +65,7 @@
   public void currentRevision() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat(c.revisions.keySet()).containsAllIn(ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.keySet()).containsAtLeastElementsIn(ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
@@ -74,7 +74,7 @@
     ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
     assertThat(c.revisions).hasSize(1);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat(c.revisions.keySet()).containsAllIn(ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.keySet()).containsAtLeastElementsIn(ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
@@ -83,7 +83,7 @@
     ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
     assertThat(c.revisions.keySet())
-        .containsAllIn(ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
+        .containsAtLeastElementsIn(ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
     assertThat(c.revisions.get(commitId(0))._number).isEqualTo(1);
     assertThat(c.revisions.get(commitId(1))._number).isEqualTo(2);
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 7d97967..2448ff881c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -147,8 +147,8 @@
         .parent(r1.getCommit())
         .parent(r2.getCommit())
         .message("Move change Merge Commit")
-        .author(admin.getIdent())
-        .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
     RevCommit c = commitBuilder.create();
     pushHead(testRepo, "refs/for/master", false, false);
 
@@ -186,7 +186,7 @@
         r.getChange().change().getDest().get(),
         Permission.ABANDON,
         systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("move not permitted");
     move(r.getChangeId(), newBranch.get());
@@ -279,9 +279,9 @@
     input.label(testLabelC, -1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
         .containsExactly(codeReviewLabel, testLabelA, testLabelB, testLabelC);
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) -1, (short) -1);
 
     // Move the change to the 'foo' branch.
@@ -290,13 +290,13 @@
     assertThat(gApi.changes().id(changeId).get().branch).isEqualTo("foo");
 
     // 'Code-Review -2' and 'Label-A -1' will be kept.
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) 0, (short) 0);
 
     // Move the change back to 'master'.
     move(changeId, "master");
     assertThat(gApi.changes().id(changeId).get().branch).isEqualTo("master");
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -2, (short) -1, (short) 0, (short) 0);
   }
 
@@ -319,9 +319,9 @@
     input.label(testLabelA, -1);
     gApi.changes().id(changeId).current().review(input);
 
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().keySet())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().keySet())
         .containsExactly(testLabelA);
-    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email).votes().values())
+    assertThat(gApi.changes().id(changeId).current().reviewer(admin.email()).votes().values())
         .containsExactly((short) -1);
 
     move(changeId, "foo");
@@ -364,7 +364,7 @@
   }
 
   private PushOneCommit.Result createChange(String branch, String changeId) throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, changeId);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, changeId);
     PushOneCommit.Result result = push.to("refs/for/" + branch);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
new file mode 100644
index 0000000..649c7ae
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class PluginFieldsIT extends AbstractPluginFieldsTest {
+  private static final Gson GSON = OutputFormat.JSON.newGson();
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
+  }
+
+  @Test
+  public void getChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
+  }
+
+  @Test
+  public void getChangeDetailWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
+  }
+
+  @Test
+  public void getChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
+  }
+
+  @Test
+  public void getChangeDetailWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id, opts))));
+  }
+
+  @Test
+  public void getChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id, opts))));
+  }
+
+  @Test
+  public void getChangeDetailWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id, opts))));
+  }
+
+  private String changeQueryUrl(Change.Id id) {
+    return changeQueryUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryUrl(Change.Id id, ImmutableListMultimap<String, String> opts) {
+    String url = "/changes/?q=" + id;
+    String queryString = buildQueryString(opts);
+    if (!queryString.isEmpty()) {
+      url += "&" + queryString;
+    }
+    return url;
+  }
+
+  private String changeUrl(Change.Id id) {
+    return changeUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeUrl(Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return changeUrl(id, "", pluginOptions);
+  }
+
+  private String changeDetailUrl(Change.Id id) {
+    return changeDetailUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeDetailUrl(
+      Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return changeUrl(id, "/detail", pluginOptions);
+  }
+
+  private String changeUrl(
+      Change.Id id, String suffix, ImmutableListMultimap<String, String> pluginOptions) {
+    String url = "/changes/" + project + "~" + id + suffix;
+    String queryString = buildQueryString(pluginOptions);
+    if (!queryString.isEmpty()) {
+      url += "?" + queryString;
+    }
+    return url;
+  }
+
+  private static String buildQueryString(ImmutableListMultimap<String, String> opts) {
+    return Joiner.on('&').withKeyValueSeparator('=').join(opts.entries());
+  }
+
+  @Nullable
+  private static List<MyInfo> pluginInfoFromSingletonList(RestResponse res) throws Exception {
+    res.assertOK();
+    List<Map<String, Object>> changeInfos =
+        GSON.fromJson(res.getReader(), new TypeToken<List<Map<String, Object>>>() {}.getType());
+    assertThat(changeInfos).hasSize(1);
+    return decodeRawPluginsList(GSON, changeInfos.get(0).get("plugins"));
+  }
+
+  @Nullable
+  private List<MyInfo> pluginInfoFromChangeInfo(RestResponse res) throws Exception {
+    res.assertOK();
+    Map<String, Object> changeInfo =
+        GSON.fromJson(res.getReader(), new TypeToken<Map<String, Object>>() {}.getType());
+    return decodeRawPluginsList(GSON, changeInfo.get("plugins"));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index 82fc426..ca4288f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -121,7 +121,7 @@
 
     TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%private");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%private");
     result.assertErrorStatus();
   }
 
@@ -133,11 +133,11 @@
     RevCommit initialHead = getRemoteHead(project2, "master");
     TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
     PushOneCommit.Result result =
-        pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master%draft");
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     result.assertErrorStatus();
 
     testRepo.reset(initialHead);
-    result = pushFactory.create(admin.getIdent(), testRepo).to("refs/drafts/master");
+    result = pushFactory.create(admin.newIdent(), testRepo).to("refs/drafts/master");
     result.assertErrorStatus();
   }
 
@@ -154,7 +154,7 @@
 
   private PushOneCommit.Result createChange(Project.NameKey proj, String ref) throws Exception {
     TestRepository<InMemoryRepository> testRepo = cloneProject(proj);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index cde75a0..2ad4aca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -76,8 +76,8 @@
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
-    assertPersonEquals(admin.getIdent(), newHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), newHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), newHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), newHead.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, headAfterFirstSubmit, newHead);
     assertChangeMergedEvents(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index ccb684c..974180c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -70,8 +70,8 @@
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change3.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getCommitterIdent());
     assertSubmittedTogether(id1, id3, id2, id1);
     assertSubmittedTogether(id2, id3, id2, id1);
     assertSubmittedTogether(id3, id3, id2, id1);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 4af27ab..9bc5a2f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -38,7 +38,7 @@
     assertThat(headAfterSubmit.getParent(0)).isEqualTo(initialHead);
     assertThat(headAfterSubmit.getParent(1)).isEqualTo(change.getCommit());
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), headAfterSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
@@ -82,7 +82,7 @@
     assertThat(headAfterSecondSubmit.getParent(0).getShortMessage())
         .isEqualTo(headAfterFirstSubmit.getShortMessage());
     assertThat(headAfterSecondSubmit.getParent(0).getId()).isEqualTo(headAfterFirstSubmit.getId());
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
 
     assertRefUpdatedEvents(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 94090a6..5ebcd85 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -71,8 +71,8 @@
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), updatedHead.getCommitterIdent());
 
     assertRefUpdatedEvents(initialHead, updatedHead);
     assertChangeMergedEvents(change.getChangeId(), updatedHead.name());
@@ -100,8 +100,8 @@
     assertThat(headAfterFirstSubmit.getShortMessage())
         .isEqualTo(change2.getCommit().getShortMessage());
     assertThat(headAfterFirstSubmit.getParent(0).getId()).isEqualTo(initialHead.getId());
-    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), headAfterFirstSubmit.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getCommitterIdent());
 
     // We need to merge changes 3, 4 and 5.
     approve(change3.getChangeId());
@@ -114,7 +114,7 @@
     assertThat(headAfterSecondSubmit.getParent(0).getShortMessage())
         .isEqualTo(change2.getCommit().getShortMessage());
 
-    assertPersonEquals(admin.getIdent(), headAfterSecondSubmit.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
 
     // First change stays untouched.
@@ -445,7 +445,7 @@
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
 
     // Push a change to master
-    PushOneCommit push = pushFactory.create(user.getIdent(), testRepo, "small fix", "a.txt", "2");
+    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
     PushOneCommit.Result change = push.to("refs/for/master");
     submit(change.getChangeId());
     RevCommit headAfterFirstSubmit = getRemoteLog(project, "master").get(0);
@@ -497,7 +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(user.getIdent(), testRepo, "small fix", "a.txt", "2");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
     PushOneCommit.Result change2result = change.to("refs/for/master");
 
     // Now cherry pick to stable
@@ -534,13 +534,13 @@
   @Test
   public void dependencyOnOutdatedPatchSetPreventsMerge() throws Exception {
     // Create a change
-    PushOneCommit change = pushFactory.create(user.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     PatchSet.Id patchSetId = changeResult.getPatchSetId();
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(user.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(user.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Create new patch set for first change.
@@ -576,12 +576,12 @@
   @Test
   public void dependencyOnDeletedChangePreventsMerge() throws Exception {
     // Create a change
-    PushOneCommit change = pushFactory.create(user.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(user.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(user.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Delete first change.
@@ -599,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();
@@ -611,13 +613,13 @@
     grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     // Create a change
-    PushOneCommit change = pushFactory.create(admin.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     approve(changeResult.getChangeId());
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(admin.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
 
     // Move the first change to a destination branch that is non-visible to user so that user cannot
@@ -630,7 +632,7 @@
     gApi.changes().id(changeResult.getChangeId()).move(secretBranch.get());
     block(secretBranch.get(), "read", ANONYMOUS_USERS);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
     try {
@@ -652,7 +654,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 '"
+            + user.username()
+            + "' or was it deleted?");
 
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -664,20 +668,20 @@
     grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     // Create a change
-    PushOneCommit change = pushFactory.create(admin.getIdent(), testRepo, "fix", "a.txt", "foo");
+    PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
     PushOneCommit.Result changeResult = change.to("refs/for/master");
     approve(changeResult.getChangeId());
 
     // Create a successor change.
     PushOneCommit change2 =
-        pushFactory.create(admin.getIdent(), testRepo, "feature", "b.txt", "bar");
+        pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
     PushOneCommit.Result change2Result = change2.to("refs/for/master");
     approve(change2Result.getChangeId());
 
     // Mark the first change private so that it's not visible to user.
     gApi.changes().id(changeResult.getChangeId()).setPrivate(true, "nobody should see this");
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
     try {
@@ -726,7 +730,7 @@
         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");
+        pushFactory.create(admin.newIdent(), repo2, "An ancestor change in repo2", "a.txt", "2");
     PushOneCommit.Result change2a = push.to("refs/for/master");
     approve(change2a.getChangeId());
     PushOneCommit.Result change2b =
@@ -737,7 +741,7 @@
     // 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());
+    requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see change2a
     try {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index 7eec854..eb8dea5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -15,25 +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.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() {
@@ -54,8 +64,8 @@
     assertCurrentRevision(change.getChangeId(), 2, head);
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change.getChangeId(), 2);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), head.getCommitterIdent());
     assertRefUpdatedEvents(oldHead, head);
     assertChangeMergedEvents(change.getChangeId(), head.name());
   }
@@ -79,36 +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",
-            (newCommitMessage, original, mergeTip, destination) -> {
-              List<String> custom = mergeTip.getFooterLines("Custom");
-              if (!custom.isEmpty()) {
-                newCommitMessage += "Custom-Parent: " + custom.get(0) + "\n";
-              }
-              return newCommitMessage + "Custom: " + destination.get();
-            });
-    try {
-      // change1 is a fast-forward, but should be rebased in cherry pick style
-      // anyway, making change2 not a fast-forward, requiring a rebase.
-      approve(change1.getChangeId());
-      submit(change2.getChangeId());
-    } finally {
-      handle.remove();
+    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2, modifier3)) {
+      ImmutableList<PushOneCommit.Result> changes = submitWithRebase(admin);
+      ChangeData cd1 = changes.get(0).getChange();
+      ChangeData cd2 = changes.get(1).getChange();
+      assertThat(cd2.patchSets()).hasSize(2);
+      String change1CurrentCommit = cd1.currentPatchSet().getRevision().get();
+      String change2Ps1Commit = cd2.patchSet(new PatchSet.Id(cd2.getId(), 1)).getRevision().get();
+
+      assertThat(gApi.changes().id(cd2.getId().get()).revision(2).commit(false).message)
+          .isEqualTo(
+              "Change 2\n\n"
+                  + ("Change-Id: " + cd2.change().getKey() + "\n")
+                  + ("Reviewed-on: "
+                      + urlFormatter.get().getChangeViewUrl(project, cd2.getId()).get()
+                      + "\n")
+                  + "Reviewed-by: Administrator <admin@example.com>\n"
+                  + ("This-change-before-rebase: " + change2Ps1Commit + "\n")
+                  + ("Previous-step-tip: " + change1CurrentCommit + "\n")
+                  + "Dest: master\n");
     }
-    // ... but both changes should get custom footers.
-    assertThat(getCurrentCommit(change1).getFooterLines("Custom"))
-        .containsExactly("refs/heads/master");
-    assertThat(getCurrentCommit(change2).getFooterLines("Custom"))
-        .containsExactly("refs/heads/master");
-    assertThat(getCurrentCommit(change2).getFooterLines("Custom-Parent"))
-        .containsExactly("refs/heads/master");
+  }
+
+  @Test
+  public void failingChangeMessageModifierShortCircuits() throws Exception {
+    ChangeMessageModifier modifier1 =
+        (msg, orig, tip, dest) -> {
+          throw new IllegalStateException("boom");
+        };
+    ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
+    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
+      try {
+        submitWithRebase();
+        assert_().fail("expected ResourceConflictException");
+      } catch (ResourceConflictException e) {
+        Throwable cause = Throwables.getRootCause(e);
+        assertThat(cause).isInstanceOf(RuntimeException.class);
+        assertThat(cause).hasMessageThat().isEqualTo("boom");
+      }
+    }
+  }
+
+  @Test
+  public void changeMessageModifierReturningNullShortCircuits() throws Exception {
+    ChangeMessageModifier modifier1 = (msg, orig, tip, dest) -> null;
+    ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
+    try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
+      try {
+        submitWithRebase();
+        assert_().fail("expected ResourceConflictException");
+      } catch (ResourceConflictException e) {
+        Throwable cause = Throwables.getRootCause(e);
+        assertThat(cause).isInstanceOf(RuntimeException.class);
+        assertThat(cause)
+            .hasMessageThat()
+            .isEqualTo(
+                modifier1.getClass().getName()
+                    + ".onSubmit from plugin modifier-1 returned null instead of new commit"
+                    + " message");
+      }
+    }
+  }
+
+  private AutoCloseable installChangeMessageModifiers(ChangeMessageModifier... modifiers) {
+    Deque<RegistrationHandle> handles = new ArrayDeque<>(modifiers.length);
+    for (int i = 0; i < modifiers.length; i++) {
+      handles.push(changeMessageModifiers.add("modifier-" + (i + 1), modifiers[i]));
+    }
+    return () -> {
+      while (!handles.isEmpty()) {
+        handles.pop().remove();
+      }
+    };
   }
 
   private void assertLatestRevisionHasFooters(PushOneCommit.Result change) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 19f1706..7bb31a0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -42,8 +42,8 @@
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
-    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
-    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertPersonEquals(admin.newIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.newIdent(), head.getCommitterIdent());
     assertRefUpdatedEvents(oldHead, head);
     assertChangeMergedEvents(change.getChangeId(), head.name());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index b4455f7..87fc9f6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.submit.ChangeSet;
 import com.google.gerrit.server.submit.MergeSuperSet;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -305,7 +304,7 @@
   }
 
   private void assertChangeSetMergeable(ChangeData change, boolean expected)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException,
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
           PermissionBackendException {
     ChangeSet cs = mergeSuperSet.get().completeChangeSet(change.change(), user(admin));
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
@@ -333,7 +332,7 @@
       List<RevCommit> parents,
       String ref)
       throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), repo, subject, fileName, content);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);
 
     if (!parents.isEmpty()) {
       push.setParents(parents);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index af206f1..9bbe1dd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -66,9 +66,11 @@
     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();
+    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
@@ -122,7 +124,9 @@
     assertThat(reviewers.get(0).account).isNotNull();
     assertThat(ImmutableList.of(reviewers.get(0).account._accountId))
         .containsAnyIn(
-            ImmutableList.of(user1, user2, user3).stream().map(u -> u.id.get()).collect(toList()));
+            ImmutableList.of(user1, user2, user3).stream()
+                .map(u -> u.id().get())
+                .collect(toList()));
   }
 
   @Test
@@ -131,23 +135,23 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
 
-    requestScopeOperations.setApiUser(user1.getId());
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    requestScopeOperations.setApiUser(user1.id());
+    reviewers = suggestReviewers(changeId, user2.fullName(), 2);
     assertThat(reviewers).isEmpty();
 
-    requestScopeOperations.setApiUser(user2.getId());
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user2.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
 
-    requestScopeOperations.setApiUser(user3.getId());
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user3.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
   }
 
   @Test
@@ -155,10 +159,10 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    requestScopeOperations.setApiUser(user3.getId());
+    requestScopeOperations.setApiUser(user3.id());
     block("refs/*", "read", ANONYMOUS_USERS);
     allow("refs/*", "read", group1);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
   }
 
@@ -168,16 +172,16 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    requestScopeOperations.setApiUser(user1.getId());
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    requestScopeOperations.setApiUser(user1.id());
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
 
     // Clear cached group info.
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     allowGlobalCapabilities(group1, GlobalCapability.VIEW_ALL_ACCOUNTS);
-    reviewers = suggestReviewers(changeId, user2.username, 2);
+    reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
   }
 
   @Test
@@ -223,27 +227,27 @@
     reviewers = suggestReviewers(changeId, name("user"));
     assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, user1.username);
+    reviewers = suggestReviewers(changeId, user1.username());
     assertThat(reviewers).hasSize(1);
 
     reviewers = suggestReviewers(changeId, "example.com");
     assertThat(reviewers).hasSize(5);
 
-    reviewers = suggestReviewers(changeId, user1.email);
+    reviewers = suggestReviewers(changeId, user1.email());
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user1.username + " example");
+    reviewers = suggestReviewers(changeId, user1.username() + " example");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user4.email.toLowerCase());
+    reviewers = suggestReviewers(changeId, user4.email().toLowerCase());
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.get(0).account.email).isEqualTo(user4.email);
+    assertThat(reviewers.get(0).account.email).isEqualTo(user4.email());
   }
 
   @Test
   public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
     String changeId = createChange().getChangeId();
-    String query = user3.username;
+    String query = user3.username();
     List<SuggestedReviewerInfo> suggestedReviewerInfos =
         gApi.changes().id(changeId).suggestReviewers(query).get();
     assertThat(suggestedReviewerInfos).hasSize(1);
@@ -333,40 +337,40 @@
     TestAccount reviewer1 = user("customuser2", "User2");
     TestAccount reviewer2 = user("customuser3", "User3");
 
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     String changeId1 = createChangeFromApi();
 
-    requestScopeOperations.setApiUser(reviewer1.getId());
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     String changeId2 = createChangeFromApi();
 
-    requestScopeOperations.setApiUser(reviewer1.getId());
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId2);
 
-    requestScopeOperations.setApiUser(reviewer2.getId());
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     String changeId3 = createChangeFromApi();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer1.id.get(), reviewer2.id.get())
+        .containsExactly(reviewer1.id().get(), reviewer2.id().get())
         .inOrder();
 
     // check that existing reviewers are filtered out
-    gApi.changes().id(changeId3).addReviewer(reviewer1.email);
+    gApi.changes().id(changeId3).addReviewer(reviewer1.email());
     reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer2.id.get())
+        .containsExactly(reviewer2.id().get())
         .inOrder();
   }
 
   @Test
   public void defaultReviewerSuggestionOnFirstChange() throws Exception {
     TestAccount user1 = user("customuser1", "User1");
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChange().getChangeId(), "", 4);
     assertThat(reviewers).isEmpty();
   }
@@ -385,23 +389,23 @@
     TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
 
     // Create a change as userWhoOwns and add some reviews
-    requestScopeOperations.setApiUser(userWhoOwns.getId());
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId1 = createChangeFromApi();
 
-    requestScopeOperations.setApiUser(reviewer1.getId());
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    requestScopeOperations.setApiUser(user1.getId());
+    requestScopeOperations.setApiUser(user1.id());
     String changeId2 = createChangeFromApi();
 
-    requestScopeOperations.setApiUser(reviewer1.getId());
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId2);
 
-    requestScopeOperations.setApiUser(reviewer2.getId());
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
     // Create a comment as a different user
-    requestScopeOperations.setApiUser(userWhoComments.getId());
+    requestScopeOperations.setApiUser(userWhoComments.id());
     ReviewInput ri = new ReviewInput();
     ri.message = "Test";
     gApi.changes().id(changeId1).revision(1).review(ri);
@@ -409,11 +413,14 @@
     // Create a change as a new user to assert that we receive the correct
     // ranking
 
-    requestScopeOperations.setApiUser(userWhoLooksForSuggestions.getId());
+    requestScopeOperations.setApiUser(userWhoLooksForSuggestions.id());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Pri", 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
         .containsExactly(
-            reviewer1.id.get(), reviewer2.id.get(), userWhoOwns.id.get(), userWhoComments.id.get())
+            reviewer1.id().get(),
+            reviewer2.id().get(),
+            userWhoOwns.id().get(),
+            userWhoComments.id().get())
         .inOrder();
   }
 
@@ -428,31 +435,31 @@
     TestAccount reviewer1 = user("customuser2", fullName);
     TestAccount reviewer2 = user("customuser3", fullName);
 
-    requestScopeOperations.setApiUser(userWhoOwns.getId());
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId1 = createChangeFromApi();
 
-    requestScopeOperations.setApiUser(reviewer1.getId());
+    requestScopeOperations.setApiUser(reviewer1.id());
     reviewChange(changeId1);
 
-    requestScopeOperations.setApiUser(userWhoOwns.getId());
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId2 = createChangeFromApi(newProject);
 
-    requestScopeOperations.setApiUser(reviewer2.getId());
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId2);
 
-    requestScopeOperations.setApiUser(userWhoOwns.getId());
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     String changeId3 = createChangeFromApi(newProject);
 
-    requestScopeOperations.setApiUser(reviewer2.getId());
+    requestScopeOperations.setApiUser(reviewer2.id());
     reviewChange(changeId3);
 
-    requestScopeOperations.setApiUser(userWhoOwns.getId());
+    requestScopeOperations.setApiUser(userWhoOwns.id());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Prim", 4);
 
     // Assert that reviewer1 is on top, even though reviewer2 has more reviews
     // in other projects
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
-        .containsExactly(reviewer1.id.get(), reviewer2.id.get())
+        .containsExactly(reviewer1.id().get(), reviewer2.id().get())
         .inOrder();
   }
 
@@ -460,17 +467,17 @@
   public void suggestNoInactiveAccounts() throws Exception {
     String name = name("foo");
     TestAccount foo1 = accountCreator.create(name + "-1");
-    assertThat(gApi.accounts().id(foo1.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
 
     TestAccount foo2 = accountCreator.create(name + "-2");
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isTrue();
+    assertThat(gApi.accounts().id(foo2.username()).getActive()).isTrue();
 
     String changeId = createChange().getChangeId();
     assertReviewers(
         suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
 
-    gApi.accounts().id(foo2.username).setActive(false);
-    assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+    gApi.accounts().id(foo2.username()).setActive(false);
+    assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
     assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
   }
 
@@ -492,7 +499,7 @@
     String secondaryEmail = "foo.secondary@example.com";
     createAccountWithSecondaryEmail("foo", secondaryEmail);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     List<SuggestedReviewerInfo> reviewers =
         suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
     assertThat(reviewers).isEmpty();
@@ -512,7 +519,7 @@
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
         .containsExactly(secondaryEmail);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
     assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
@@ -524,7 +531,7 @@
     EmailInput input = new EmailInput();
     input.email = secondaryEmail;
     input.noConfirmation = true;
-    gApi.accounts().id(foo.id.get()).addEmail(input);
+    gApi.accounts().id(foo.id().get()).addEmail(input);
     return foo;
   }
 
@@ -577,13 +584,12 @@
       List<TestAccount> expectedUsers,
       List<AccountGroup.UUID> expectedGroups) {
     List<Integer> actualAccountIds =
-        actual
-            .stream()
+        actual.stream()
             .filter(i -> i.account != null)
             .map(i -> i.account._accountId)
             .collect(toList());
     assertThat(actualAccountIds)
-        .containsExactlyElementsIn(expectedUsers.stream().map(u -> u.id.get()).collect(toList()));
+        .containsExactlyElementsIn(expectedUsers.stream().map(u -> u.id().get()).collect(toList()));
 
     List<String> actualGroupIds =
         actual.stream().filter(i -> i.group != null).map(i -> i.group.id).collect(toList());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 259c4d2..49692dd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -48,10 +48,10 @@
 
   @After
   public void tearDown() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    requestScopeOperations.setApiUser(admin.id());
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.workInProgressByDefault = false;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   @Test
@@ -145,9 +145,9 @@
   }
 
   private void setWorkInProgressByDefaultForUser() throws Exception {
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
     prefs.workInProgressByDefault = true;
-    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
   }
 
   private PushOneCommit.Result createChange(Project.NameKey p) throws Exception {
@@ -156,7 +156,7 @@
 
   private PushOneCommit.Result createChange(Project.NameKey p, String r) throws Exception {
     TestRepository<InMemoryRepository> testRepo = cloneProject(p);
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(r);
     result.assertOkStatus();
     return result;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index 3f76a27..99fdbc8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -36,14 +36,14 @@
   @Test
   public void confirm() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
+    in.token = emailTokenVerifier.encode(admin.id(), "new.mail@example.com");
     adminRestSession.put("/config/server/email.confirm", in).assertNoContent();
   }
 
   @Test
   public void confirmForOtherUser_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
+    in.token = emailTokenVerifier.encode(user.id(), "new.mail@example.com");
     adminRestSession.put("/config/server/email.confirm", in).assertUnprocessableEntity();
   }
 
@@ -57,7 +57,7 @@
   @Test
   public void confirmAlreadyInUse_UnprocessableEntity() throws Exception {
     ConfirmEmail.Input in = new ConfirmEmail.Input();
-    in.token = emailTokenVerifier.encode(admin.getId(), user.email);
+    in.token = emailTokenVerifier.encode(admin.id(), user.email());
     adminRestSession.put("/config/server/email.confirm", in).assertUnprocessableEntity();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index c19f5d0..2a891aa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -36,8 +36,7 @@
     r.consume();
 
     Optional<String> id =
-        result
-            .stream()
+        result.stream()
             .filter(t -> "Log File Compressor".equals(t.command))
             .map(t -> t.id)
             .findFirst();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 95bc5a6..3d987a3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -188,7 +188,7 @@
     if (force) {
       testRepo.reset(initialHead);
     }
-    commit(user.getIdent(), "subject");
+    commit(user.newIdent(), "subject");
 
     boolean createTag = tagName == null;
     tagName = MoreObjects.firstNonNull(tagName, "v1_" + System.nanoTime());
@@ -197,9 +197,9 @@
         break;
       case ANNOTATED:
         if (createTag) {
-          createAnnotatedTag(testRepo, tagName, user.getIdent());
+          createAnnotatedTag(testRepo, tagName, user.newIdent());
         } else {
-          updateAnnotatedTag(testRepo, tagName, user.getIdent());
+          updateAnnotatedTag(testRepo, tagName, user.newIdent());
         }
         break;
       default:
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index d1a80c7..72af075 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -171,7 +171,7 @@
   public void createAccessChange() throws Exception {
     allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
     // User can see the branch
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
 
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -186,7 +186,7 @@
     accessSection.permissions.put(Permission.READ, read);
     accessInput.add.put(REFS_HEADS, accessSection);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ChangeInfo out = pApi().accessChange(accessInput);
 
     assertThat(out.project).isEqualTo(newProjectName.get());
@@ -194,7 +194,7 @@
     assertThat(out.status).isEqualTo(ChangeStatus.NEW);
     assertThat(out.submitted).isNull();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
 
     ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
     assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
@@ -205,7 +205,7 @@
     gApi.changes().id(out._number).current().submit();
 
     // check that the change took effect.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       BranchInfo info = pApi().branch("refs/heads/master").get();
       fail("wanted failure, got " + newGson().toJson(info));
@@ -216,16 +216,16 @@
     // Restore.
     accessInput.add.clear();
     accessInput.remove.put(REFS_HEADS, accessSection);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     out = pApi().accessChange(accessInput);
 
     gApi.changes().id(out._number).current().review(reviewIn);
     gApi.changes().id(out._number).current().submit();
 
     // Now it works again.
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
   }
 
@@ -325,7 +325,7 @@
     accessInput.add.put(REFS_ALL, accessSectionInfo);
     pApi().access(accessInput);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -345,7 +345,7 @@
     AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
     accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -408,7 +408,7 @@
     ProjectAccessInput accessInput = newProjectAccessInput();
     accessInput.parent = newParentProjectName;
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     exception.expectMessage("administrate server not permitted");
     pApi().access(accessInput);
@@ -435,7 +435,7 @@
 
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -455,7 +455,7 @@
                 .get(AccessSection.GLOBAL_CAPABILITIES)
                 .permissions
                 .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+        .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
   }
 
   @Test
@@ -491,7 +491,7 @@
 
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -517,7 +517,7 @@
                 .get(AccessSection.GLOBAL_CAPABILITIES)
                 .permissions
                 .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+        .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
 
     // Remove
     accessInput.add.clear();
@@ -561,7 +561,7 @@
     config = cfg.toText();
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
+            admin.newIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
     push.to(RefNames.REFS_CONFIG).assertOkStatus();
 
     // Verify that unknownPermission is present
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index 7af7d04..131c24a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -34,7 +34,6 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 8943125..c06ec69 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -63,7 +63,7 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -81,7 +81,7 @@
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertCreateSucceeds(testBranch);
   }
 
@@ -95,7 +95,7 @@
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden() throws Exception {
     grantOwner();
     blockCreateReference();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -113,7 +113,7 @@
     allow(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH, REGISTERED_USERS);
     assertCreateFails(
         new Branch.NameKey(allUsers, RefNames.refsUsers(new Account.Id(1))),
-        RefNames.refsUsers(admin.getId()),
+        RefNames.refsUsers(admin.id()),
         ResourceConflictException.class,
         "Not allowed to create user branch.");
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index cd29bf8..d8fbbe5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -121,7 +121,7 @@
         Future<RestResponse> r1 = executor.submit(createProjectFoo);
         Future<RestResponse> r2 = executor.submit(createProjectFoo);
         assertThat(ImmutableList.of(r1.get().getStatusCode(), r2.get().getStatusCode()))
-            .containsAllOf(HttpStatus.SC_CREATED, HttpStatus.SC_CONFLICT);
+            .containsAtLeast(HttpStatus.SC_CREATED, HttpStatus.SC_CONFLICT);
       }
     } finally {
       executor.shutdown();
@@ -182,6 +182,28 @@
   }
 
   @Test
+  public void createProjectThatEndsWithSlash() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName + "/").get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void createProjectThatContainsSlash() throws Exception {
+    String newProjectName = name("newProject/newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName).get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
   public void createProjectWithProperties() throws Exception {
     String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
@@ -303,7 +325,7 @@
   public void createProjectWithCapability() throws Exception {
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
@@ -316,7 +338,7 @@
 
   @Test
   public void createProjectWithoutCapability_Forbidden() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     ProjectInput in = new ProjectInput();
     in.name = name("newProject");
     assertCreateFails(in, AuthException.class);
@@ -335,7 +357,7 @@
     parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      requestScopeOperations.setApiUser(user.getId());
+      requestScopeOperations.setApiUser(user.id());
       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 8ca6b75..8c96662 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -51,7 +51,7 @@
 
   @Test
   public void deleteBranch_Forbidden() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden(testBranch);
   }
 
@@ -63,7 +63,7 @@
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -77,21 +77,21 @@
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithDeletePermission() throws Exception {
     grantDelete();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -138,7 +138,7 @@
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("Not allowed to delete user branch.");
-    branch(new Branch.NameKey(allUsers, RefNames.refsUsers(admin.id))).delete();
+    branch(new Branch.NameKey(allUsers, RefNames.refsUsers(admin.id()))).delete();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index a85004e..47b6a78 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -74,14 +74,14 @@
 
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       project().deleteBranches(input);
       fail("Expected AuthException");
     } catch (AuthException e) {
       assertThat(e).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
     }
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
 
@@ -89,14 +89,14 @@
   public void deleteMultiBranchesWithoutPermissionForbidden() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       project().deleteBranches(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     }
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 27de4b1..07bb2b1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -44,7 +44,7 @@
 
   @Test
   public void deleteTag_Forbidden() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden();
   }
 
@@ -56,7 +56,7 @@
   @Test
   public void deleteTagByProjectOwner() throws Exception {
     grantOwner();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
@@ -70,21 +70,21 @@
   public void deleteTagByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteForbidden();
   }
 
   @Test
   public void deleteTagByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
   @Test
   public void deleteTagByUserWithDeletePermission() throws Exception {
     grantDelete();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertDeleteSucceeds();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index 6d8689a..fae9d00 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -65,14 +65,14 @@
   public void deleteTagsForbidden() throws Exception {
     DeleteTagsInput input = new DeleteTagsInput();
     input.tags = TAGS;
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     try {
       project().deleteTags(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     }
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     assertTags(TAGS);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 1963455..18c706b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -85,7 +85,7 @@
   @Test
   public void getOpenChange_Found() throws Exception {
     unblockRead();
-    PushOneCommit.Result r = pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     r.assertOkStatus();
 
     CommitInfo info = getCommit(r.getCommit());
@@ -107,7 +107,7 @@
 
   @Test
   public void getOpenChange_NotFound() throws Exception {
-    PushOneCommit.Result r = pushFactory.create(admin.getIdent(), testRepo).to("refs/for/master");
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
     r.assertOkStatus();
     assertNotFound(r.getCommit());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 7f2b35e..ec1c708 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -42,7 +42,7 @@
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead("refs/*");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).branches().get();
   }
@@ -74,7 +74,7 @@
     blockRead("refs/heads/dev");
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(
         ImmutableList.of(
@@ -87,7 +87,7 @@
     blockRead("refs/heads/master");
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(ImmutableList.of(branch("refs/heads/dev", dev, false)), list().get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index a0bc450..7746820 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -53,6 +53,17 @@
   }
 
   @Test
+  public void listChildrenWithLimit() throws Exception {
+    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 {
     String prefix = RandomStringUtils.randomAlphabetic(8);
     Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 14dbffb..d9b8e03 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -17,9 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.util.stream.Collectors.toList;
 
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestProjectInput;
@@ -33,11 +37,19 @@
 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.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
 import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
 import org.junit.Test;
 
 @NoHttpd
@@ -45,6 +57,7 @@
 public class ListProjectsIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ListProjects listProjects;
 
   @Test
   public void listProjects() throws Exception {
@@ -56,7 +69,7 @@
 
   @Test
   public void listProjectsFiltersInvisibleProjects() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -92,19 +105,95 @@
 
   @Test
   public void listProjectsWithLimit() throws Exception {
+    ProjectCacheImpl projectCacheImpl = (ProjectCacheImpl) projectCache;
     String pre = "lpwl-someProject";
     int n = 6;
     for (int i = 0; i < n; i++) {
       projectOperations.newProject().name(pre + i).create();
     }
 
+    projectCacheImpl.evictAllByName();
     for (int i = 1; i <= n + 2; i++) {
       assertThatNameList(gApi.projects().list().withPrefix(pre).withLimit(i).get())
           .hasSize(Math.min(i, n));
+      assertThat(projectCacheImpl.sizeAllByName())
+          .isAtMost((long) (i + 2)); // 2 = AllProjects + AllUsers
     }
   }
 
   @Test
+  @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+  public void listProjectsFromIndexShouldBeLimitedTo500() throws Exception {
+    int numTestProjects = 501;
+    assertThat(createProjects("foo", numTestProjects)).hasSize(numTestProjects);
+    assertThat(gApi.projects().list().get()).hasSize(500);
+  }
+
+  @Test
+  public void listProjectsShouldNotBeLimitedByDefault() throws Exception {
+    int numTestProjects = 501;
+    assertThat(createProjects("foo", numTestProjects)).hasSize(numTestProjects);
+    assertThat(gApi.projects().list().get().size()).isAtLeast(numTestProjects);
+  }
+
+  @Test
+  public void listProjectsToOutputStream() throws Exception {
+    int numInitialProjects = gApi.projects().list().get().size();
+    int numTestProjects = 5;
+    List<String> testProjects = createProjects("zzz_testProject", numTestProjects);
+    try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+      listProjects.setStart(numInitialProjects);
+      listProjects.displayToStream(displayOut);
+
+      List<String> lines =
+          Splitter.on("\n").omitEmptyStrings().splitToList(new String(displayOut.toByteArray()));
+      assertThat(lines).isEqualTo(testProjects);
+    }
+  }
+
+  @Test
+  public void listProjectsAsJsonMultilineToOutputStream() throws Exception {
+    listProjectsAsJsonToOutputStream(OutputFormat.JSON);
+  }
+
+  @Test
+  public void listProjectsAsJsonCompactToOutputStream() throws Exception {
+    String jsonOutput = listProjectsAsJsonToOutputStream(OutputFormat.JSON_COMPACT).trim();
+    assertThat(jsonOutput).doesNotContain("\n");
+  }
+
+  private String listProjectsAsJsonToOutputStream(OutputFormat jsonFormat) throws Exception {
+    assertThat(jsonFormat.isJson()).isTrue();
+
+    int numInitialProjects = gApi.projects().list().get().size();
+    int numTestProjects = 5;
+    Set<String> testProjects =
+        ImmutableSet.copyOf(createProjects("zzz_testProject", numTestProjects));
+    try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+      listProjects.setStart(numInitialProjects);
+      listProjects.setFormat(jsonFormat);
+      listProjects.displayToStream(displayOut);
+
+      String projectsJsonOutput = new String(displayOut.toByteArray());
+
+      Gson gson = jsonFormat.newGson();
+      Set<String> projectsJsonNames = gson.fromJson(projectsJsonOutput, JsonObject.class).keySet();
+      assertThat(projectsJsonNames).isEqualTo(testProjects);
+
+      return projectsJsonOutput;
+    }
+  }
+
+  private List<String> createProjects(String prefix, int numProjects) {
+    return IntStream.range(0, numProjects)
+        .mapToObj(i -> projectOperations.newProject().name(prefix + i).create())
+        .map(Project.NameKey::get)
+        .collect(toList());
+  }
+
+  @Test
   public void listProjectsWithPrefix() throws Exception {
     Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
     Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
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 9088afa..bf2a534 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -47,7 +47,7 @@
     cfg.setString("s2", "ss", "k2", "v2");
     PushOneCommit push =
         pushFactory.create(
-            admin.getIdent(), testRepo, "Create Project Level Config", configName, cfg.toText());
+            admin.newIdent(), testRepo, "Create Project Level Config", configName, cfg.toText());
     push.to(RefNames.REFS_CONFIG);
 
     ProjectState state = projectCache.get(project);
@@ -72,7 +72,7 @@
 
     pushFactory
         .create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Create Project Level Config",
             configName,
@@ -91,7 +91,7 @@
 
     pushFactory
         .create(
-            admin.getIdent(),
+            admin.newIdent(),
             childTestRepo,
             "Create Project Level Config",
             configName,
@@ -125,7 +125,7 @@
 
     pushFactory
         .create(
-            admin.getIdent(),
+            admin.newIdent(),
             testRepo,
             "Create Project Level Config",
             configName,
@@ -150,7 +150,7 @@
 
     pushFactory
         .create(
-            admin.getIdent(),
+            admin.newIdent(),
             childTestRepo,
             "Create Project Level Config",
             configName,
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index c0f3732..0165ead 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -75,7 +75,7 @@
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
     blockRead("refs/*");
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tags().get();
   }
@@ -130,7 +130,7 @@
   public void listTagsOfNonVisibleBranch() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push1 = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push1 = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
     TagInput tag1 = new TagInput();
@@ -141,7 +141,7 @@
     assertThat(result.revision).isEqualTo(tag1.revision);
 
     pushTo("refs/heads/hidden");
-    PushOneCommit push2 = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r2 = push2.to("refs/heads/hidden");
     r2.assertOkStatus();
 
@@ -170,7 +170,7 @@
   public void lightweightTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
@@ -191,7 +191,7 @@
     assertThat(result.canDelete).isTrue();
     assertThat(result.created).isEqualTo(timestamp(r));
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     result = tag(input.ref).get();
     assertThat(result.canDelete).isNull();
 
@@ -202,7 +202,7 @@
   public void annotatedTag() throws Exception {
     grantTagPermissions();
 
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
 
@@ -215,8 +215,8 @@
     assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
     assertThat(result.object).isEqualTo(input.revision);
     assertThat(result.message).isEqualTo(input.message);
-    assertThat(result.tagger.name).isEqualTo(admin.fullName);
-    assertThat(result.tagger.email).isEqualTo(admin.email);
+    assertThat(result.tagger.name).isEqualTo(admin.fullName());
+    assertThat(result.tagger.email).isEqualTo(admin.email());
     assertThat(result.created).isEqualTo(result.tagger.date);
 
     eventRecorder.assertRefUpdatedEvents(project.get(), result.ref, null, result.revision);
@@ -230,8 +230,8 @@
     assertThat(result2.ref).isEqualTo(input2.ref);
     assertThat(result2.object).isEqualTo(input2.revision);
     assertThat(result2.message).isEqualTo(input2.message);
-    assertThat(result2.tagger.name).isEqualTo(admin.fullName);
-    assertThat(result2.tagger.email).isEqualTo(admin.email);
+    assertThat(result2.tagger.name).isEqualTo(admin.fullName());
+    assertThat(result2.tagger.email).isEqualTo(admin.email());
     assertThat(result2.created).isEqualTo(result2.tagger.date);
 
     eventRecorder.assertRefUpdatedEvents(project.get(), result2.ref, null, result2.revision);
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
new file mode 100644
index 0000000..fa1b467
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -0,0 +1,357 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.AccountVisibility;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.Result;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class AccountResolverIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setEnum("accounts", null, "visibility", AccountVisibility.SAME_GROUP);
+    return cfg;
+  }
+
+  @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private AccountOperations accountOperations;
+  @Inject private AccountResolver accountResolver;
+  @Inject private Provider<CurrentUser> self;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private Sequences sequences;
+
+  @Test
+  public void bySelf() throws Exception {
+    assertThat(resolve("Self")).isEmpty();
+    accountOperations.newAccount().fullname("self").create();
+
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).containsExactly(admin.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+
+    result = resolveAsResult("me");
+    assertThat(result.asIdSet()).containsExactly(admin.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+
+    requestScopeOperations.setApiUserAnonymous();
+    checkBySelfFails();
+
+    requestScopeOperations.setApiUserInternal();
+    checkBySelfFails();
+  }
+
+  private void checkBySelfFails() throws Exception {
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).isEmpty();
+    assertThat(result.isSelf()).isTrue();
+    try {
+      result.asUnique();
+      assert_().fail("expected UnresolvableAccountException");
+    } catch (UnresolvableAccountException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
+      assertThat(e.isSelf()).isTrue();
+    }
+
+    result = resolveAsResult("me");
+    assertThat(result.asIdSet()).isEmpty();
+    assertThat(result.isSelf()).isTrue();
+    try {
+      result.asUnique();
+      assert_().fail("expected UnresolvableAccountException");
+    } catch (UnresolvableAccountException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
+      assertThat(e.isSelf()).isTrue();
+    }
+  }
+
+  @Test
+  public void bySelfInactive() throws Exception {
+    gApi.accounts().id(user.id().get()).setActive(false);
+
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(gApi.accounts().id("self").getActive()).isFalse();
+
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).containsExactly(user.id());
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameInstanceAs(self.get());
+  }
+
+  @Test
+  public void byExactAccountId() throws Exception {
+    Account.Id existingId = accountOperations.newAccount().create();
+    Account.Id idWithExistingIdAsFullname =
+        accountOperations.newAccount().fullname(existingId.toString()).create();
+
+    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+    assertThat(resolve(existingId)).containsExactly(existingId);
+    assertThat(resolve(nonexistentId)).isEmpty();
+
+    assertThat(resolveByNameOrEmail(existingId)).containsExactly(idWithExistingIdAsFullname);
+  }
+
+  @Test
+  public void byParenthesizedAccountId() throws Exception {
+    Account.Id existingId = accountOperations.newAccount().fullname("Test User").create();
+    accountOperations.newAccount().fullname(existingId.toString()).create();
+
+    Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+    accountOperations.newAccount().fullname("Any Name (" + nonexistentId + ")").create();
+    accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+    String existingInput = "Any Name (" + existingId + ")";
+    assertThat(resolve(existingInput)).containsExactly(existingId);
+    assertThat(resolve("Any Name (" + nonexistentId + ")")).isEmpty();
+
+    assertThat(resolveByNameOrEmail(existingInput)).isEmpty();
+  }
+
+  @Test
+  public void byUsername() throws Exception {
+    String existingUsername = "myusername";
+    Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create();
+    Account.Id idWithExistingUsernameAsFullname =
+        accountOperations.newAccount().fullname(existingUsername).create();
+
+    String nonexistentUsername = "anotherusername";
+    Account.Id idWithFullname = accountOperations.newAccount().fullname("anotherusername").create();
+
+    assertThat(resolve(existingUsername)).containsExactly(idWithUsername);
+
+    // Doesn't short-circuit just because the input looks like a valid username.
+    assertThat(ExternalId.isValidUsername(nonexistentUsername)).isTrue();
+    assertThat(resolve(nonexistentUsername)).containsExactly(idWithFullname);
+
+    assertThat(resolveByNameOrEmail(existingUsername))
+        .containsExactly(idWithExistingUsernameAsFullname);
+  }
+
+  @Test
+  public void byNameAndEmail() throws Exception {
+    String email = name("user@example.com");
+    Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+    accountOperations.newAccount().fullname(email).create();
+
+    String input = "First Last <" + email + ">";
+    assertThat(resolve(input)).containsExactly(idWithEmail);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(idWithEmail);
+  }
+
+  @Test
+  public void byNameAndEmailPrefersAccountsWithMatchingFullName() throws Exception {
+    String email = name("user@example.com");
+    Account.Id id1 = accountOperations.newAccount().fullname("Aaa Bbb").create();
+    setPreferredEmailBypassingUniquenessCheck(id1, email);
+    Account.Id id2 = accountOperations.newAccount().fullname("Ccc Ddd").create();
+    setPreferredEmailBypassingUniquenessCheck(id2, email);
+
+    String input = "First Last <" + email + ">";
+    assertThat(resolve(input)).containsExactly(id1, id2);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id1, id2);
+
+    Account.Id id3 = accountOperations.newAccount().fullname("First Last").create();
+    setPreferredEmailBypassingUniquenessCheck(id3, email);
+    assertThat(resolve(input)).containsExactly(id3);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id3);
+
+    Account.Id id4 = accountOperations.newAccount().fullname("First Last").create();
+    setPreferredEmailBypassingUniquenessCheck(id4, email);
+    assertThat(resolve(input)).containsExactly(id3, id4);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id3, id4);
+  }
+
+  @Test
+  public void byEmail() throws Exception {
+    String email = name("user@example.com");
+    Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+    accountOperations.newAccount().fullname(email).create();
+
+    assertThat(resolve(email)).containsExactly(idWithEmail);
+    assertThat(resolveByNameOrEmail(email)).containsExactly(idWithEmail);
+  }
+
+  // Can't test for ByRealm because DefaultRealm with the default (OPENID) auth type doesn't support
+  // email expansion, so anything that would return a non-null value from DefaultRealm#lookup would
+  // just be an email address, handled by other tests. This could be avoided if we inject some sort
+  // of custom test realm instance, but the ugliness is not worth it for this small bit of test
+  // coverage.
+
+  @Test
+  public void byFullName() throws Exception {
+    Account.Id id1 = accountOperations.newAccount().fullname("Somebodys Name").create();
+    accountOperations.newAccount().fullname("A totally different name").create();
+    String input = "Somebodys name";
+    assertThat(resolve(input)).containsExactly(id1);
+    assertThat(resolveByNameOrEmail(input)).containsExactly(id1);
+  }
+
+  @Test
+  public void byDefaultSearch() throws Exception {
+    Account.Id id1 = accountOperations.newAccount().fullname("John Doe").create();
+    Account.Id id2 = accountOperations.newAccount().fullname("Jane Doe").create();
+    assertThat(resolve("doe")).containsExactly(id1, id2);
+    assertThat(resolveByNameOrEmail("doe")).containsExactly(id1, id2);
+  }
+
+  @Test
+  public void onlyExactIdReturnsInactiveAccounts() throws Exception {
+    TestAccount account =
+        accountOperations
+            .account(
+                accountOperations
+                    .newAccount()
+                    .fullname("Inactiveuser Name")
+                    .preferredEmail("inactiveuser@example.com")
+                    .username("inactiveusername")
+                    .create())
+            .get();
+    Account.Id id = account.accountId();
+    String nameEmail = account.fullname().get() + " <" + account.preferredEmail().get() + ">";
+    ImmutableList<String> inputs =
+        ImmutableList.of(
+            account.fullname().get() + " (" + account.accountId() + ")",
+            account.fullname().get(),
+            account.preferredEmail().get(),
+            nameEmail,
+            Splitter.on(' ').splitToList(account.fullname().get()).get(0));
+
+    assertThat(resolve(account.accountId())).containsExactly(id);
+    for (String input : inputs) {
+      assertThat(resolve(input)).named("results for %s (active)", input).containsExactly(id);
+    }
+
+    gApi.accounts().id(id.get()).setActive(false);
+    assertThat(resolve(account.accountId())).containsExactly(id);
+    for (String input : inputs) {
+      Result result = accountResolver.resolve(input);
+      assertThat(result.asIdSet()).named("results for %s (inactive)", input).isEmpty();
+      try {
+        result.asUnique();
+        assert_().fail("expected UnresolvableAccountException");
+      } catch (UnresolvableAccountException e) {
+        assertThat(e)
+            .hasMessageThat()
+            .isEqualTo(
+                "Account '"
+                    + input
+                    + "' only matches inactive accounts. To use an inactive account, retry"
+                    + " with one of the following exact account IDs:\n"
+                    + id
+                    + ": "
+                    + nameEmail);
+      }
+      assertThat(resolveByNameOrEmail(input))
+          .named("results by name or email for %s (inactive)", input)
+          .isEmpty();
+    }
+  }
+
+  @Test
+  public void filterVisibility() throws Exception {
+    Account.Id id1 =
+        accountOperations
+            .newAccount()
+            .fullname("John Doe")
+            .preferredEmail("johndoe@example.com")
+            .create();
+    Account.Id id2 =
+        accountOperations
+            .newAccount()
+            .fullname("Jane Doe")
+            .preferredEmail("janedoe@example.com")
+            .create();
+
+    // Admin can see all accounts. Use a variety of searches, including with/without
+    // callerMayAssumeCandidatesAreVisible.
+    assertThat(resolve(id1)).containsExactly(id1);
+    assertThat(resolve("John Doe")).containsExactly(id1);
+    assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+    assertThat(resolve(id2)).containsExactly(id2);
+    assertThat(resolve("Jane Doe")).containsExactly(id2);
+    assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+    assertThat(resolve("doe")).containsExactly(id1, id2);
+
+    // id2 can't see id1, and vice versa.
+    requestScopeOperations.setApiUser(id1);
+    assertThat(resolve(id1)).containsExactly(id1);
+    assertThat(resolve("John Doe")).containsExactly(id1);
+    assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+    assertThat(resolve(id2)).isEmpty();
+    assertThat(resolve("Jane Doe")).isEmpty();
+    assertThat(resolve("janedoe@example.com")).isEmpty();
+    assertThat(resolve("doe")).containsExactly(id1);
+
+    requestScopeOperations.setApiUser(id2);
+    assertThat(resolve(id1)).isEmpty();
+    assertThat(resolve("John Doe")).isEmpty();
+    assertThat(resolve("johndoe@example.com")).isEmpty();
+    assertThat(resolve(id2)).containsExactly(id2);
+    assertThat(resolve("Jane Doe")).containsExactly(id2);
+    assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+    assertThat(resolve("doe")).containsExactly(id2);
+  }
+
+  private ImmutableSet<Account.Id> resolve(Object input) throws Exception {
+    return resolveAsResult(input).asIdSet();
+  }
+
+  private Result resolveAsResult(Object input) throws Exception {
+    return accountResolver.resolve(input.toString());
+  }
+
+  @SuppressWarnings("deprecation")
+  private ImmutableSet<Account.Id> resolveByNameOrEmail(Object input) throws Exception {
+    return accountResolver.resolveByNameOrEmail(input.toString()).asIdSet();
+  }
+
+  private void setPreferredEmailBypassingUniquenessCheck(Account.Id id, String email)
+      throws Exception {
+    Optional<AccountState> result =
+        accountsUpdateProvider
+            .get()
+            .update("Force set preferred email", id, (s, u) -> u.setPreferredEmail(email));
+    assertThat(result.map(a -> a.getAccount().getPreferredEmail())).hasValue(email);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/BUILD b/javatests/com/google/gerrit/acceptance/server/account/BUILD
new file mode 100644
index 0000000..48fac99
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_account",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 0a4d972..15dd3fb 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -27,7 +27,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.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -87,7 +86,7 @@
 
   @Before
   public void setUp() {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
   }
 
   @Test
@@ -143,7 +142,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -167,7 +166,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -199,7 +198,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -273,7 +272,7 @@
   public void listComments() throws Exception {
     String file = "file";
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "first subject", file, "contents");
+        pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "contents");
     PushOneCommit.Result r = push.to("refs/for/master");
     String changeId = r.getChangeId();
     String revId = r.getCommit().getName();
@@ -382,7 +381,7 @@
       String file = "file";
       String contents = "contents " + line;
       PushOneCommit push =
-          pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
+          pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
@@ -425,7 +424,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "content")
             .to("refs/for/master");
     changeId = r2.getChangeId();
     revId = r2.getCommit().getName();
@@ -440,10 +439,10 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
@@ -453,13 +452,13 @@
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts();
     assertThat(actual.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> comments = actual.get(FILE_NAME);
@@ -486,7 +485,7 @@
 
     PushOneCommit.Result r2 =
         pushFactory
-            .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
             .to("refs/for/master");
 
     addComment(r1, "nit: trailing whitespace");
@@ -499,14 +498,14 @@
     assertThat(comments).hasSize(2);
 
     CommentInfo c1 = comments.get(0);
-    assertThat(c1.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c1.author._accountId).isEqualTo(user.id().get());
     assertThat(c1.patchSet).isEqualTo(1);
     assertThat(c1.message).isEqualTo("nit: trailing whitespace");
     assertThat(c1.side).isNull();
     assertThat(c1.line).isEqualTo(1);
 
     CommentInfo c2 = comments.get(1);
-    assertThat(c2.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c2.author._accountId).isEqualTo(user.id().get());
     assertThat(c2.patchSet).isEqualTo(2);
     assertThat(c2.message).isEqualTo("typo: content");
     assertThat(c2.side).isNull();
@@ -527,18 +526,26 @@
 
   @Test
   public void publishCommentsAllRevisions() throws Exception {
-    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r1 =
+        pushFactory
+            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
+            .to("refs/for/master");
 
     PushOneCommit.Result r2 =
         pushFactory
             .create(
-                admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new\ncntent\n", r1.getChangeId())
+                admin.newIdent(),
+                testRepo,
+                SUBJECT,
+                FILE_NAME,
+                "new interesting\ncntent\n",
+                r1.getChangeId())
             .to("refs/for/master");
 
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
-        newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+        newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 10), "Is it that bad?"));
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
@@ -546,7 +553,7 @@
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
-        newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
+        newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 15), "better now"));
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
@@ -567,11 +574,11 @@
         other.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     // Drafts by other users aren't returned.
     addDraft(
         r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
@@ -587,7 +594,7 @@
     assertThat(ps1List).hasSize(2);
     assertThat(ps1List.get(0).message).isEqualTo("what happened to this?");
     assertThat(ps1List.get(0).side).isEqualTo(Side.PARENT);
-    assertThat(ps1List.get(1).message).isEqualTo("nit: trailing whitespace");
+    assertThat(ps1List.get(1).message).isEqualTo("Is it that bad?");
     assertThat(ps1List.get(1).side).isNull();
 
     assertThat(gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).drafts())
@@ -599,7 +606,7 @@
     assertThat(ps2List).hasSize(4);
     assertThat(ps2List.get(0).message).isEqualTo("comment 1 on base");
     assertThat(ps2List.get(1).message).isEqualTo("comment 2 on base");
-    assertThat(ps2List.get(2).message).isEqualTo("join lines");
+    assertThat(ps2List.get(2).message).isEqualTo("better now");
     assertThat(ps2List.get(3).message).isEqualTo("typo: content");
 
     List<Message> messages = email.getMessages(r2.getChangeId(), "comment");
@@ -632,8 +639,8 @@
                 + "#/c/"
                 + c
                 + "/1/a.txt@1 \n"
-                + "PS1, Line 1: ew\n"
-                + "nit: trailing whitespace\n"
+                + "PS1, Line 1: boring\n"
+                + "Is it that bad?\n"
                 + "\n"
                 + "\n"
                 + url
@@ -662,8 +669,8 @@
                 + "#/c/"
                 + c
                 + "/2/a.txt@1 \n"
-                + "PS2, Line 1: ew\n"
-                + "join lines\n"
+                + "PS2, Line 1: interesting\n"
+                + "better now\n"
                 + "\n"
                 + "\n"
                 + url
@@ -734,8 +741,7 @@
     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));
@@ -745,8 +751,6 @@
       assertThat(changeInfo2.totalCommentCount).isEqualTo(2);
       assertThat(changeInfo3.unresolvedCommentCount).isEqualTo(1);
       assertThat(changeInfo3.totalCommentCount).isEqualTo(2);
-    } finally {
-      enableDb(ctx);
     }
   }
 
@@ -764,7 +768,7 @@
     String uuid = commentsMap.get(targetComment.path).get(0).id;
     DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
   }
@@ -835,7 +839,7 @@
     // PS4 has comments [c7, c8].
     assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     for (int i = 0; i < commentsBeforeDelete.size(); i++) {
       List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
@@ -850,7 +854,7 @@
           gApi.changes().id(changeId).revision(patchSet).comment(uuid).delete(input);
 
       String expectedMsg =
-          String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason);
+          String.format("Comment removed by: %s; Reason: %s", admin.fullName(), input.reason);
       assertThat(updatedComment.message).isEqualTo(expectedMsg);
       oldComment.message = expectedMsg;
       assertThat(updatedComment).isEqualTo(oldComment);
@@ -905,7 +909,7 @@
 
     List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     for (int i = 0; i < 3; i++) {
       DeleteCommentInput input = new DeleteCommentInput("delete comment 2, iteration: " + i);
       gApi.changes().id(changeId).revision(ps1).comment(uuid).delete(input);
@@ -914,7 +918,8 @@
     CommentInfo updatedComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get();
     String expectedMsg =
         String.format(
-            "Comment removed by: %s; Reason: %s", admin.fullName, "delete comment 2, iteration: 2");
+            "Comment removed by: %s; Reason: %s",
+            admin.fullName(), "delete comment 2, iteration: 2");
     assertThat(updatedComment.message).isEqualTo(expectedMsg);
     oldComment.message = expectedMsg;
     assertThat(updatedComment).isEqualTo(oldComment);
@@ -938,9 +943,7 @@
   }
 
   private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
-    return getPublishedComments(changeId, revId)
-        .values()
-        .stream()
+    return getPublishedComments(changeId, revId).values().stream()
         .flatMap(List::stream)
         .collect(toList());
   }
@@ -1097,30 +1100,49 @@
     return populate(d, path, side, null, line, message, false);
   }
 
+  private DraftInput newDraft(String path, Side side, Comment.Range range, String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, side, null, range, message, false);
+  }
+
   private DraftInput newDraftOnParent(String path, int parent, int line, String message) {
     DraftInput d = new DraftInput();
     return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message, false);
   }
 
   private static <C extends Comment> C populate(
-      C c, String path, Side side, Integer parent, int line, String message, Boolean unresolved) {
+      C c,
+      String path,
+      Side side,
+      Integer parent,
+      Comment.Range range,
+      String message,
+      Boolean unresolved) {
+    int line = range.startLine;
     c.path = path;
     c.side = side;
     c.parent = parent;
     c.line = line != 0 ? line : null;
     c.message = message;
     c.unresolved = unresolved;
-    if (line != 0) {
-      Comment.Range range = new Comment.Range();
-      range.startLine = line;
-      range.startCharacter = 1;
-      range.endLine = line;
-      range.endCharacter = 5;
-      c.range = range;
-    }
+    if (line != 0) c.range = range;
     return c;
   }
 
+  private static <C extends Comment> C populate(
+      C c, String path, Side side, Integer parent, int line, String message, Boolean unresolved) {
+    return populate(c, path, side, parent, createLineRange(line, 1, 5), message, unresolved);
+  }
+
+  private static Comment.Range createLineRange(int line, int startChar, int endChar) {
+    Comment.Range range = new Comment.Range();
+    range.startLine = line;
+    range.startCharacter = startChar;
+    range.endLine = line;
+    range.endCharacter = endChar;
+    return range;
+  }
+
   private static Function<CommentInfo, CommentInput> infoToInput(String path) {
     return infoToInput(path, CommentInput::new);
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 3073022..7c375bd 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -26,7 +26,6 @@
 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;
@@ -36,19 +35,19 @@
 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.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+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.TestChanges;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -96,7 +95,7 @@
         serverSideTestRepo
             .getRevWalk()
             .parseCommit(serverSideTestRepo.getRepository().exactRef("HEAD").getObjectId());
-    adminId = admin.getId();
+    adminId = admin.id();
     checker = checkerProvider.get();
   }
 
@@ -115,9 +114,9 @@
   public void missingOwner() throws Exception {
     TestAccount owner = accountCreator.create("missing");
     ChangeNotes notes = insertChange(owner);
-    deleteUserBranch(owner.getId());
+    deleteUserBranch(owner.id());
 
-    assertProblems(notes, null, problem("Missing change owner: " + owner.getId()));
+    assertProblems(notes, null, problem("Missing change owner: " + owner.id()));
   }
 
   // No test for ref existing but object missing; InMemoryRepository won't let
@@ -234,7 +233,7 @@
 
   @Test
   public void onlyPatchSetObjectMissingWithFix() throws Exception {
-    Change c = TestChanges.newChange(project, admin.getId(), sequences.nextChangeId());
+    Change c = TestChanges.newChange(project, admin.id(), sequences.nextChangeId());
 
     PatchSet.Id psId = c.currentPatchSetId();
     String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
@@ -313,7 +312,7 @@
           notes.getChangeId(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               ctx.getChange().setStatus(Change.Status.MERGED);
               ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
               return true;
@@ -379,7 +378,7 @@
             "Marked change as merged"));
 
     notes = reload(notes);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(notes.getChange().isMerged()).isTrue();
     assertNoProblems(notes, null);
   }
 
@@ -422,7 +421,7 @@
             "Marked change as merged"));
 
     notes = reload(notes);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(notes.getChange().isMerged()).isTrue();
     assertNoProblems(notes, null);
   }
 
@@ -572,7 +571,7 @@
     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(notes.getChange().isMerged()).isTrue();
     assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps2.getId(), psId3);
     assertThat(psUtil.get(notes, psId3).getRevision().get()).isEqualTo(rev1);
   }
@@ -620,7 +619,7 @@
     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(notes.getChange().isMerged()).isTrue();
     assertThat(psUtil.byChangeAsMap(notes).keySet())
         .containsExactly(ps1.getId(), ps3.getId(), psId4);
     assertThat(psUtil.get(notes, psId4).getRevision().get()).isEqualTo(rev2);
@@ -657,7 +656,7 @@
 
     notes = reload(notes);
     assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId2);
-    assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+    assertThat(notes.getChange().isMerged()).isTrue();
     assertThat(psUtil.byChangeAsMap(notes).keySet()).containsExactly(ps1.getId(), psId2);
     assertThat(psUtil.get(notes, psId2).getRevision().get()).isEqualTo(rev2);
   }
@@ -747,13 +746,13 @@
   private ChangeNotes insertChange(TestAccount owner, String dest) throws Exception {
     Change.Id id = new Change.Id(sequences.nextChangeId());
     ChangeInserter ins;
-    try (BatchUpdate bu = newUpdate(owner.getId())) {
+    try (BatchUpdate bu = newUpdate(owner.id())) {
       RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+      bu.setNotify(NotifyResolver.Result.none());
       ins =
           changeInserterFactory
               .create(id, commit, dest)
               .setValidate(false)
-              .setNotify(NotifyHandling.NONE)
               .setFireRevisionCreated(false)
               .setSendMail(false);
       bu.insertChange(ins).execute();
@@ -773,12 +772,12 @@
   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);
@@ -830,8 +829,7 @@
 
   private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
     PersonIdent committer = serverIdent.get();
-    PersonIdent author =
-        noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
+    PersonIdent author = noteUtil.newIdent(getAccount(admin.id()), committer.getWhen(), committer);
     serverSideTestRepo
         .branch(RefNames.changeMetaRef(id))
         .commit()
@@ -863,7 +861,7 @@
             }
 
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               ctx.getChange().setStatus(Change.Status.MERGED);
               ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED);
               return true;
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index b581977..b9b7ab3 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -22,6 +22,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -33,6 +35,7 @@
 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;
@@ -50,12 +53,12 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -99,7 +102,7 @@
 
   @Test
   public void getRelatedNoResult() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo);
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     assertRelated(push.to("refs/for/master").getPatchSetId());
   }
 
@@ -618,6 +621,46 @@
     assertRelated(lastPsId, expected);
   }
 
+  @Test
+  public void stateOfRelatedChangesMatchesDocumentedValues() throws Exception {
+    // Set up three related changes, one new, the other abandoned, and the third merged.
+    RevCommit commit1 =
+        commitBuilder().add("a.txt", "File content 1").message("Subject 1").create();
+    RevCommit commit2 =
+        commitBuilder().add("b.txt", "File content 2").message("Subject 2").create();
+    RevCommit commit3 =
+        commitBuilder().add("c.txt", "File content 3").message("Subject 3").create();
+    pushHead(testRepo, "refs/for/master", false);
+    Change change1 = getChange(commit1).change();
+    Change change2 = getChange(commit2).change();
+    Change change3 = getChange(commit3).change();
+    gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(change1.getChangeId()).current().submit();
+    gApi.changes().id(change2.getChangeId()).abandon();
+
+    List<RelatedChangeAndCommitInfo> relatedChanges =
+        gApi.changes().id(change3.getChangeId()).current().related().changes;
+
+    // Ensure that our REST API returns the states exactly as documented (and required by the
+    // frontend).
+    assertThat(relatedChanges)
+        .comparingElementsUsing(getRelatedChangeToStatusCorrespondence())
+        .containsExactly("NEW", "ABANDONED", "MERGED");
+  }
+
+  private static Correspondence<RelatedChangeAndCommitInfo, String>
+      getRelatedChangeToStatusCorrespondence() {
+    return Correspondence.from(
+        new BinaryPredicate<RelatedChangeAndCommitInfo, String>() {
+          @Override
+          public boolean apply(
+              RelatedChangeAndCommitInfo relatedChangeAndCommitInfo, String status) {
+            return Objects.equals(relatedChangeAndCommitInfo.status, status);
+          }
+        },
+        "has status");
+  }
+
   private RevCommit parseBody(RevCommit c) throws Exception {
     testRepo.getRevWalk().parseBody(c);
     return c;
@@ -650,7 +693,7 @@
           psId.getParentKey(),
           new BatchUpdateOp() {
             @Override
-            public boolean updateChange(ChangeContext ctx) throws OrmException {
+            public boolean updateChange(ChangeContext ctx) {
               PatchSet ps = psUtil.get(ctx.getNotes(), psId);
               psUtil.setGroups(ctx.getUpdate(psId), ps, ImmutableList.of());
               return true;
@@ -682,6 +725,7 @@
       assertThat(a._currentRevisionNumber)
           .named("current revision of " + name)
           .isEqualTo(e._currentRevisionNumber);
+      assertThat(a.status).isEqualTo(e.status);
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 580b5de..4e5cb5e 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -254,7 +254,7 @@
     PatchListCacheImpl.LargeObjectTombstone tombstone =
         new PatchListCacheImpl.LargeObjectTombstone();
     abstractPatchListCache.put(key, tombstone);
-    assertThat(abstractPatchListCache.getIfPresent(key)).isSameAs(tombstone);
+    assertThat(abstractPatchListCache.getIfPresent(key)).isSameInstanceAs(tombstone);
   }
 
   private static void assertAdded(String expectedNewName, PatchListEntry e) {
@@ -296,9 +296,7 @@
 
   private static PatchListEntry getEntryFor(PatchList patchList, String filePath) {
     Optional<PatchListEntry> patchListEntry =
-        patchList
-            .getPatches()
-            .stream()
+        patchList.getPatches().stream()
             .filter(entry -> entry.getNewName().equals(filePath))
             .findAny();
     return patchListEntry.orElseThrow(
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index 79cb097..768c269 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -36,8 +36,8 @@
   protected MailMessage.Builder messageBuilderWithDefaultFields() {
     MailMessage.Builder b = MailMessage.builder();
     b.id("some id");
-    b.from(user.emailAddress);
-    b.addTo(user.emailAddress); // Not evaluated
+    b.from(user.getEmailAddress());
+    b.addTo(user.getEmailAddress()); // Not evaluated
     b.subject("");
     b.dateReceived(Instant.now());
     return b;
@@ -52,12 +52,12 @@
     String file = "gerrit-server/test.txt";
     String contents = "contents \nlorem \nipsum \nlorem";
     PushOneCommit push =
-        pushFactory.create(admin.getIdent(), testRepo, "first subject", file, contents);
+        pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
     PushOneCommit.Result r = push.to("refs/for/master");
     String changeId = r.getChangeId();
 
     // Review it
-    requestScopeOperations.setApiUser(reviewer.getId());
+    requestScopeOperations.setApiUser(reviewer.id());
     ReviewInput input = new ReviewInput();
     input.message = "I have two comments";
     input.comments = new HashMap<>();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 4b6e8c6..abf02d5 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -47,6 +47,7 @@
 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;
@@ -54,6 +55,8 @@
 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;
 
@@ -77,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();
+    }
   }
 
   /*
@@ -99,6 +105,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -113,6 +120,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -128,6 +136,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -143,6 +152,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -154,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
@@ -170,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
@@ -179,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
@@ -206,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
@@ -226,6 +239,7 @@
         .bcc(sc.starrer)
         .bcc(ABANDONED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void abandon(String changeId, TestAccount by) throws Exception {
@@ -246,7 +260,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     AbandonInput in = new AbandonInput();
     if (notify != null) {
       in.notify = notify;
@@ -261,7 +275,7 @@
   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);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email());
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -269,6 +283,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -284,7 +299,7 @@
   private void addReviewerToReviewableChangeByOwnerCcingSelf(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, null);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, null);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -292,6 +307,7 @@
         .cc(sc.owner, sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -308,7 +324,7 @@
     TestAccount other = accountCreator.create("other", "other@example.com", "other");
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, other, reviewer.email);
+    addReviewer(adder, sc.changeId, other, reviewer.email());
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -316,6 +332,7 @@
         .cc(sc.owner, sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -332,7 +349,7 @@
     TestAccount other = accountCreator.create("other", "other@example.com", "other");
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, other, reviewer.email, CC_ON_OWN_COMMENTS, null);
+    addReviewer(adder, sc.changeId, other, reviewer.email(), CC_ON_OWN_COMMENTS, null);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -340,6 +357,7 @@
         .cc(sc.owner, sc.reviewer, other)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -363,6 +381,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -378,8 +397,8 @@
   private void addReviewerToWipChange(Adder adder) throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email());
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -392,27 +411,38 @@
     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 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);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), NotifyHandling.ALL);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -420,6 +450,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -435,7 +466,7 @@
   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);
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), OWNER_REVIEWERS);
     // TODO(logan): Should CCs be included?
     assertThat(sender)
         .sent("newchange", sc)
@@ -443,6 +474,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -459,8 +491,8 @@
       throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, OWNER);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, OWNER);
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -477,8 +509,8 @@
       throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
-    addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, NONE);
-    assertThat(sender).notSent();
+    addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, NONE);
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -500,6 +532,7 @@
         .cc(sc.reviewer)
         .cc(sc.ccerByEmail, sc.reviewerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -521,6 +554,7 @@
         .cc(sc.reviewer)
         .cc(sc.ccerByEmail, sc.reviewerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -589,7 +623,7 @@
       @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     adder.addReviewer(changeId, reviewer, notify);
   }
 
@@ -608,6 +642,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -622,6 +657,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -636,6 +672,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -650,6 +687,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -665,6 +703,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -680,6 +719,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -691,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
@@ -705,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
@@ -720,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
@@ -734,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
@@ -761,6 +803,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -769,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
@@ -777,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
@@ -792,6 +837,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -805,6 +851,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -812,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
@@ -827,7 +874,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -842,13 +889,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void addReviewerOnWipChangeAndStartReview() throws Exception {
     StagedChange sc = stageWipChange();
-    ReviewInput in = ReviewInput.noScore().reviewer(other.email).setWorkInProgress(false);
+    ReviewInput in = ReviewInput.noScore().reviewer(other.email()).setWorkInProgress(false);
     gApi.changes().id(sc.changeId).revision("current").review(in);
     assertThat(sender)
         .sent("comment", sc)
@@ -864,7 +911,7 @@
         .cc(sc.reviewer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -918,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
@@ -931,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
@@ -939,41 +987,41 @@
     // Make sure owner user is created
     StagedChange sc = stageReviewableChange();
     // All was cleaned already
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
 
     // Toggle workInProgress flag for owner
-    GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id().get()).getPreferences();
     prefs.workInProgressByDefault = true;
-    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+    gApi.accounts().id(sc.owner.id().get()).setPreferences(prefs);
 
     // Create another change without notification that should be wip
     StagedPreChange spc = stagePreChange("refs/for/master");
     Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
 
     // Clean up workInProgressByDefault by owner
-    prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    prefs = gApi.accounts().id(sc.owner.id().get()).getPreferences();
     Truth.assertThat(prefs.workInProgressByDefault).isTrue();
     prefs.workInProgressByDefault = false;
-    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+    gApi.accounts().id(sc.owner.id().get()).setPreferences(prefs);
   }
 
   @Test
   public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER_REVIEWERS");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void createReviewableChangeWithNotifyOwner() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void createReviewableChangeWithNotifyNone() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER");
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -984,6 +1032,7 @@
         .to(spc.watchingProjectOwner)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -991,11 +1040,13 @@
     StagedPreChange spc =
         stagePreChange(
             "refs/for/master",
-            users -> ImmutableList.of("r=" + users.reviewer.username, "cc=" + users.ccer.username));
+            users ->
+                ImmutableList.of("r=" + users.reviewer.username(), "cc=" + users.ccer.username()));
     FakeEmailSenderSubject subject =
         assertThat(sender).sent("newchange", spc).to(spc.reviewer, spc.watchingProjectOwner);
     subject.cc(spc.ccer);
     subject.bcc(NEW_CHANGES, NEW_PATCHSETS).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1012,6 +1063,7 @@
         .cc("nobody2@example.com")
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   /*
@@ -1021,7 +1073,7 @@
   @Test
   public void deleteReviewerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1031,6 +1083,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1046,12 +1099,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1061,13 +1115,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeByAdminCcingSelf() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1077,12 +1132,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteCcerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraCcer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1092,12 +1148,13 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1105,13 +1162,14 @@
         .cc(extraCcer, sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1120,13 +1178,14 @@
     setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
     assertThat(sender).sent("deleteReviewer", sc).to(sc.owner, extraReviewer).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1134,27 +1193,27 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
     removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     removeReviewer(sc, extraReviewer);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerFromWipChangeNotifyAll() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer, NotifyHandling.ALL);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1164,15 +1223,17 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteReviewerWithApprovalFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     removeReviewer(sc, extraReviewer);
     assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1180,18 +1241,18 @@
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   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 {
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend());
   }
 
@@ -1203,15 +1264,18 @@
     StagedChange sc = stager.stage();
     ReviewInput in =
         ReviewInput.noScore()
-            .reviewer(extraReviewer.email)
-            .reviewer(extraCcer.email, ReviewerState.CC, false);
-    requestScopeOperations.setApiUser(extraReviewer.getId());
+            .reviewer(extraReviewer.email())
+            .reviewer(extraCcer.email(), ReviewerState.CC, false);
+    requestScopeOperations.setApiUser(extraReviewer.id());
     gApi.changes().id(sc.changeId).revision("current").review(in);
+    sender.clear();
     return sc;
   }
 
   private StagedChange stageReviewableChangeWithExtraReviewer() throws Exception {
-    return stageChangeWithExtraReviewer(this::stageReviewableChange);
+    StagedChange sc = stageChangeWithExtraReviewer(this::stageReviewableChange);
+    sender.clear();
+    return sc;
   }
 
   private StagedChange stageReviewableWipChangeWithExtraReviewer() throws Exception {
@@ -1219,12 +1283,14 @@
   }
 
   private StagedChange stageWipChangeWithExtraReviewer() throws Exception {
-    return stageChangeWithExtraReviewer(this::stageWipChange);
+    StagedChange sc = stageChangeWithExtraReviewer(this::stageWipChange);
+    assertThat(sender).didNotSend();
+    return sc;
   }
 
   private void removeReviewer(StagedChange sc, TestAccount account) throws Exception {
     sender.clear();
-    gApi.changes().id(sc.changeId).reviewer(account.email).remove();
+    gApi.changes().id(sc.changeId).reviewer(account.email()).remove();
   }
 
   private void removeReviewer(StagedChange sc, TestAccount account, NotifyHandling notify)
@@ -1232,7 +1298,7 @@
     sender.clear();
     DeleteReviewerInput in = new DeleteReviewerInput();
     in.notify = notify;
-    gApi.changes().id(sc.changeId).reviewer(account.email).remove(in);
+    gApi.changes().id(sc.changeId).reviewer(account.email()).remove(in);
   }
 
   /*
@@ -1243,7 +1309,7 @@
   public void deleteVoteFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1252,6 +1318,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1259,7 +1326,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1269,13 +1336,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1285,6 +1353,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1292,7 +1361,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1302,19 +1371,21 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
         .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1322,7 +1393,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1330,24 +1401,26 @@
         .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER);
     assertThat(sender).sent("deleteVote", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1355,16 +1428,16 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1373,13 +1446,14 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void deleteVoteFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1388,11 +1462,12 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void deleteVote(StagedChange sc, TestAccount account) throws Exception {
     sender.clear();
-    gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote("Code-Review");
+    gApi.changes().id(sc.changeId).reviewer(account.email()).deleteVote("Code-Review");
   }
 
   private void deleteVote(StagedChange sc, TestAccount account, NotifyHandling notify)
@@ -1401,7 +1476,7 @@
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
     in.notify = notify;
-    gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote(in);
+    gApi.changes().id(sc.changeId).reviewer(account.email()).deleteVote(in);
   }
 
   /*
@@ -1409,16 +1484,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
@@ -1433,6 +1539,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1447,6 +1554,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1461,6 +1569,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1473,6 +1582,7 @@
         .cc(sc.reviewer, sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1480,6 +1590,7 @@
     StagedChange sc = stageChangeReadyForMerge();
     merge(sc.changeId, other, OWNER);
     assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1488,13 +1599,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
@@ -1502,7 +1614,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 {
@@ -1512,7 +1624,7 @@
   private void merge(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(changeId).revision("current").submit();
   }
 
@@ -1524,7 +1636,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     SubmitInput in = new SubmitInput();
     in.notify = notify;
     gApi.changes().id(changeId).revision("current").submit(in);
@@ -1532,7 +1644,7 @@
 
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
-    requestScopeOperations.setApiUser(sc.reviewer.getId());
+    requestScopeOperations.setApiUser(sc.reviewer.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     sender.clear();
     return sc;
@@ -1554,6 +1666,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1569,6 +1682,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1584,6 +1698,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1598,6 +1713,7 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1613,13 +1729,14 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .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
@@ -1628,7 +1745,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
@@ -1637,28 +1754,28 @@
     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
@@ -1673,6 +1790,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1687,20 +1805,21 @@
         .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 newPatchSetOnReviewableChangeAddingReviewer() throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
+    pushTo(sc, "refs/for/master%r=" + newReviewer.username(), sc.owner);
     assertThat(sender)
         .sent("newpatchset", sc)
         .to(sc.reviewer, newReviewer)
@@ -1709,22 +1828,22 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetOnWipChangeAddingReviewer() throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
-    assertThat(sender).notSent();
+    pushTo(sc, "refs/for/master%r=" + newReviewer.username(), sc.owner);
+    assertThat(sender).didNotSend();
   }
 
   @Test
   public void newPatchSetOnWipChangeAddingReviewerNotifyAll() throws Exception {
     StagedChange sc = stageWipChange();
     TestAccount newReviewer = sc.testAccount("newReviewer");
-    pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username, sc.owner);
+    pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username(), sc.owner);
     assertThat(sender)
         .sent("newpatchset", sc)
         .to(sc.reviewer, newReviewer)
@@ -1733,7 +1852,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1748,7 +1867,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
-    assertThat(sender).notSent();
+    assertThat(sender).didNotSend();
   }
 
   private void pushTo(StagedChange sc, String ref, TestAccount by) throws Exception {
@@ -1758,7 +1877,7 @@
   private void pushTo(StagedChange sc, String ref, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    pushFactory.create(by.getIdent(), sc.repo, sc.changeId).to(ref).assertOkStatus();
+    pushFactory.create(by.newIdent(), sc.repo, sc.changeId).to(ref).assertOkStatus();
   }
 
   @Test
@@ -1773,6 +1892,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1787,6 +1907,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1801,6 +1922,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1813,6 +1935,7 @@
         .cc(sc.ccer)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1826,6 +1949,7 @@
         .cc(sc.ccer, other)
         .cc(sc.reviewerByEmail, sc.ccerByEmail)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1833,6 +1957,7 @@
     StagedChange sc = stageReviewableChange();
     editCommitMessage(sc, other, OWNER);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1840,27 +1965,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
@@ -1868,6 +1994,7 @@
     StagedChange sc = stageWipChange();
     editCommitMessage(sc, other);
     assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1875,6 +2002,7 @@
     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
@@ -1889,6 +2017,7 @@
         .bcc(sc.starrer)
         .bcc(NEW_PATCHSETS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void editCommitMessage(StagedChange sc, TestAccount by) throws Exception {
@@ -1931,6 +2060,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1944,6 +2074,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1957,6 +2088,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1971,6 +2103,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1985,6 +2118,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -1999,6 +2133,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private void restore(String changeId, TestAccount by) throws Exception {
@@ -2008,7 +2143,7 @@
   private void restore(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(changeId).restore();
   }
 
@@ -2037,6 +2172,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2061,6 +2197,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2085,6 +2222,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2109,11 +2247,12 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   private StagedChange stageChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     gApi.changes().id(sc.changeId).revision("current").submit();
     sender.clear();
@@ -2127,7 +2266,7 @@
   private void revert(StagedChange sc, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     gApi.changes().id(sc.changeId).revert();
   }
 
@@ -2144,6 +2283,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2156,6 +2296,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2167,6 +2308,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2179,6 +2321,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2189,6 +2332,7 @@
         .sent("setassignee", sc)
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2203,6 +2347,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2215,6 +2360,7 @@
         .sent("setassignee", sc)
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2226,6 +2372,7 @@
         .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
         .to(sc.assignee)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2237,6 +2384,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 {
@@ -2246,9 +2394,9 @@
   private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.getId());
+    requestScopeOperations.setApiUser(by.id());
     AssigneeInput in = new AssigneeInput();
-    in.assignee = to.email;
+    in.assignee = to.email();
     gApi.changes().id(sc.changeId).setAssignee(in);
   }
 
@@ -2267,6 +2415,7 @@
         .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
         .noOneElse();
+    assertThat(sender).didNotSend();
   }
 
   @Test
@@ -2282,20 +2431,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 {
-    requestScopeOperations.setApiUser(sc.owner.getId());
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).setReadyForReview();
-    // PolyGerrit current immediately follows up with a review.
-    gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
   }
 
   private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 8ad9f96..1386aec 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -62,7 +62,7 @@
   @Test
   public void metadataOnNewChange() throws Exception {
     PushOneCommit.Result newChange = createChange();
-    gApi.changes().id(newChange.getChangeId()).addReviewer(user.getId().toString());
+    gApi.changes().id(newChange.getChangeId()).addReviewer(user.id().toString());
 
     List<FakeEmailSender.Message> emails = sender.getMessages();
     assertThat(emails).hasSize(1);
@@ -89,14 +89,14 @@
   @Test
   public void metadataOnNewComment() throws Exception {
     PushOneCommit.Result newChange = createChange();
-    gApi.changes().id(newChange.getChangeId()).addReviewer(user.getId().toString());
+    gApi.changes().id(newChange.getChangeId()).addReviewer(user.id().toString());
     sender.clear();
 
     // Review change
     ReviewInput input = new ReviewInput();
     input.message = "Test";
     revision(newChange).review(input);
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     Collection<ChangeMessageInfo> result =
         gApi.changes().id(newChange.getChangeId()).get().messages;
     assertThat(result).isNotEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index b8380f5..f917fd8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -165,7 +165,7 @@
     b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     // Set account state to inactive
-    accountOperations.account(user.id).forUpdate().inactive().update();
+    accountOperations.account(user.id()).forUpdate().inactive().update();
 
     mailProcessor.process(b.build());
     comments = gApi.changes().id(changeId).current().commentsAsList();
@@ -189,7 +189,7 @@
         newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
     MailMessage.Builder b =
         messageBuilderWithDefaultFields()
-            .from(user.emailAddress)
+            .from(user.getEmailAddress())
             .textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     sender.clear();
@@ -211,7 +211,7 @@
         newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
     MailMessage.Builder b =
         messageBuilderWithDefaultFields()
-            .from(user.emailAddress)
+            .from(user.getEmailAddress())
             .textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     sender.clear();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 8d21b5b..c395c81 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -41,7 +41,7 @@
     // Check that the user's email was added as Reply-To
     assertThat(sender.getMessages()).hasSize(1);
     Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
-    assertThat(headerString(headers, "Reply-To")).contains(user.email);
+    assertThat(headerString(headers, "Reply-To")).contains(user.email());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index 43a3642..628b90c 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -34,11 +34,11 @@
     // Set user preference to receive only plaintext content
     GeneralPreferencesInfo i = new GeneralPreferencesInfo();
     i.emailFormat = EmailFormat.PLAINTEXT;
-    gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    gApi.accounts().id(admin.id().toString()).setPreferences(i);
 
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received only plaintext content
@@ -46,20 +46,20 @@
     FakeEmailSender.Message m = sender.getMessages().get(0);
     assertThat(m.body()).isNotNull();
     assertThat(m.htmlBody()).isNull();
-    assertMailReplyTo(m, admin.email);
-    assertMailReplyTo(m, user.email);
+    assertMailReplyTo(m, admin.email());
+    assertMailReplyTo(m, user.email());
 
     // Reset user preference
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     i.emailFormat = EmailFormat.HTML_PLAINTEXT;
-    gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    gApi.accounts().id(admin.id().toString()).setPreferences(i);
   }
 
   @Test
   public void userReceivesHtmlAndPlaintextEmail() throws Exception {
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received both HTML and plaintext content
@@ -67,7 +67,7 @@
     FakeEmailSender.Message m = sender.getMessages().get(0);
     assertThat(m.body()).isNotNull();
     assertThat(m.htmlBody()).isNotNull();
-    assertMailReplyTo(m, admin.email);
-    assertMailReplyTo(m, user.email);
+    assertMailReplyTo(m, admin.email());
+    assertMailReplyTo(m, user.email());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index a61b7a6..748c4ea 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -276,7 +276,7 @@
   }
 
   private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) {
-    return buf.create(project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs());
+    return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.nowTs());
   }
 
   private Optional<ObjectId> getRef(String name) throws Exception {
@@ -286,11 +286,7 @@
   }
 
   private List<String> getMessages(Change.Id id) throws Exception {
-    return gApi.changes()
-        .id(id.get())
-        .get(MESSAGES)
-        .messages
-        .stream()
+    return gApi.changes().id(id.get()).get(MESSAGES).messages.stream()
         .map(m -> m.message)
         .collect(toList());
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
index a72cd33..2919e5f 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
@@ -163,10 +163,10 @@
   }
 
   private CurrentUser user() {
-    return identifiedUserFactory.create(user.id);
+    return identifiedUserFactory.create(user.id());
   }
 
   private CurrentUser admin() {
-    return identifiedUserFactory.create(admin.id);
+    return identifiedUserFactory.create(admin.id());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 53e21b6..6cbe40e 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -178,7 +178,7 @@
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     ReviewInput input = new ReviewInput().label(P.getName(), 0);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 03f580a..4ed16ee 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -63,19 +63,19 @@
 
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "original subject", "a", "a1")
+            .create(admin.newIdent(), testRepo, "original subject", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "super sekret subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "super sekret subject", "a", "a2", r.getChangeId())
             .to("refs/for/master");
     r.assertOkStatus();
 
     r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "back to original subject", "a", "a3")
+            .create(admin.newIdent(), testRepo, "back to original subject", "a", "a3")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -104,13 +104,13 @@
     sender.clear();
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "private change", "a", "a1")
+            .create(admin.newIdent(), testRepo, "private change", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -134,14 +134,14 @@
     }
 
     PushOneCommit.Result r =
-        pushFactory.create(admin.getIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     sender.clear();
 
     r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -165,13 +165,13 @@
     sender.clear();
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "wip change", "a", "a1")
+            .create(admin.newIdent(), testRepo, "wip change", "a", "a1")
             .to("refs/for/master%wip");
     r.assertOkStatus();
 
     assertThat(sender.getMessages()).isEmpty();
 
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -194,14 +194,14 @@
     }
 
     PushOneCommit.Result r =
-        pushFactory.create(admin.getIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
+        pushFactory.create(admin.newIdent(), testRepo, "subject", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     sender.clear();
 
     r =
         pushFactory
-            .create(admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
+            .create(admin.newIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
             .to("refs/for/master%wip");
     r.assertOkStatus();
 
@@ -212,16 +212,16 @@
   public void watchProject() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a change to watched project -> should trigger email notification
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -232,7 +232,7 @@
         cloneProject(new Project.NameKey(notWatchedProject), admin);
     r =
         pushFactory
-            .create(admin.getIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
+            .create(admin.newIdent(), notWatchedRepo, "DONT_TRIGGER", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -240,7 +240,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
@@ -249,7 +249,7 @@
   public void watchFile() throws Exception {
     String watchedProject = projectOperations.newProject().create().get();
     String otherWatchedProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // watch file in project as user
     watch(watchedProject, "file:a.txt");
@@ -259,12 +259,12 @@
 
     // push a change to watched file -> should trigger email notification for
     // user
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -272,21 +272,21 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     watch(watchedProject);
 
     // push a change to non-watched file -> should not trigger email
     // notification for user, only for user2
     r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "TRIGGER_USER2", "b.txt", "b1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -294,7 +294,7 @@
     messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user2.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user2.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER_USER2\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
@@ -302,18 +302,18 @@
   @Test
   public void watchKeyword() throws Exception {
     String watchedProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
     watch(watchedProject, "multimaster");
 
     // push a change with keyword -> should trigger email notification
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), watchedRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -321,7 +321,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
@@ -329,7 +329,7 @@
     // push a change without keyword -> should not trigger email notification
     r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "Cleanup cache implementation", "b.txt", "b1")
+            .create(admin.newIdent(), watchedRepo, "Cleanup cache implementation", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -340,24 +340,24 @@
   @Test
   public void watchAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // watch the All-Projects project to watch all projects
     watch(allProjects.get());
 
     // push a change to any project -> should trigger email notification
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
-        pushFactory.create(admin.getIdent(), anyRepo, "TRIGGER", "a", "a1").to("refs/for/master");
+        pushFactory.create(admin.newIdent(), anyRepo, "TRIGGER", "a", "a1").to("refs/for/master");
     r.assertOkStatus();
 
     // assert email notification
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
@@ -365,7 +365,7 @@
   @Test
   public void watchFileAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // watch file in All-Projects project as user to watch the file in all
     // projects
@@ -373,12 +373,12 @@
 
     // push a change to watched file in any project -> should trigger email
     // notification for user
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
+            .create(admin.newIdent(), anyRepo, "TRIGGER", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -386,21 +386,21 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    requestScopeOperations.setApiUser(user2.getId());
+    requestScopeOperations.setApiUser(user2.id());
     watch(anyProject);
 
     // push a change to non-watched file in any project -> should not trigger
     // email notification for user, only for user2
     r =
         pushFactory
-            .create(admin.getIdent(), anyRepo, "TRIGGER_USER2", "b.txt", "b1")
+            .create(admin.newIdent(), anyRepo, "TRIGGER_USER2", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -408,7 +408,7 @@
     messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user2.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user2.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER_USER2\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
@@ -416,19 +416,19 @@
   @Test
   public void watchKeywordAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
 
     // watch keyword in project as user
     watch(allProjects.get(), "multimaster");
 
     // push a change with keyword to any project -> should trigger email
     // notification
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
+            .create(admin.newIdent(), anyRepo, "Document multimaster setup", "a.txt", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -436,7 +436,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
     assertThat(m.body()).contains("Change subject: Document multimaster setup\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
     sender.clear();
@@ -445,7 +445,7 @@
     // notification
     r =
         pushFactory
-            .create(admin.getIdent(), anyRepo, "Cleanup cache implementation", "b.txt", "b1")
+            .create(admin.newIdent(), anyRepo, "Cleanup cache implementation", "b.txt", "b1")
             .to("refs/for/master");
     r.assertOkStatus();
 
@@ -457,27 +457,27 @@
   public void watchProjectNoNotificationForIgnoredChange() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a change to watched project
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "ignored change", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "ignored change", "a", "a1")
             .to("refs/for/master");
     r.assertOkStatus();
 
     // ignore the change
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
 
     // post a comment -> should not trigger email notification since user ignored the change
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -490,16 +490,16 @@
   public void watchProjectNoNotificationForPrivateChange() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // push a private change to watched project -> should not trigger email notification
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "private change", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "private change", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -522,24 +522,24 @@
         new AccountGroup.UUID(groupThatCanViewPrivateChanges.id));
 
     // watch project as user that can't view private changes
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     watch(watchedProject);
 
     // watch project as user that can view all private change
     TestAccount userThatCanViewPrivateChanges =
         accountCreator.create(
             "user2", "user2@test.com", "User2", groupThatCanViewPrivateChanges.name);
-    requestScopeOperations.setApiUser(userThatCanViewPrivateChanges.getId());
+    requestScopeOperations.setApiUser(userThatCanViewPrivateChanges.id());
     watch(watchedProject);
 
     // push a private change to watched project -> should trigger email notification for
     // userThatCanViewPrivateChanges, but not for user
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
         pushFactory
-            .create(admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+            .create(admin.newIdent(), watchedRepo, "TRIGGER", "a", "a1")
             .to("refs/for/master%private");
     r.assertOkStatus();
 
@@ -547,7 +547,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.emailAddress);
+    assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.getEmailAddress());
     assertThat(m.body()).contains("Change subject: TRIGGER\n");
     assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index c61b7ad..da3a257 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -84,7 +84,7 @@
 
   @Test
   public void regularUserIsNotAllowedToGetReflog() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).branch("master").reflog();
   }
@@ -100,13 +100,13 @@
       u.save();
     }
 
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 
   @Test
   public void adminUserIsAllowedToGetReflog() throws Exception {
-    requestScopeOperations.setApiUser(admin.getId());
+    requestScopeOperations.setApiUser(admin.id());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
index dea83ca..286e5ae 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -57,7 +57,7 @@
 
   @Before
   public void setUp() {
-    identifiedAdmin = identifiedUserFactory.create(admin.id);
+    identifiedAdmin = identifiedUserFactory.create(admin.id());
     resetToStrict(quotaEnforcer);
   }
 
@@ -73,10 +73,10 @@
   @Test
   public void requestTokenForUserAndAccount() {
     QuotaRequestContext ctx =
-        QuotaRequestContext.builder().user(identifiedAdmin).account(user.id).build();
+        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"))
+    assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
index 31a8808..8b9ffc5 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -63,7 +63,7 @@
 
   @Before
   public void setUp() {
-    identifiedAdmin = identifiedUserFactory.create(admin.id);
+    identifiedAdmin = identifiedUserFactory.create(admin.id());
     resetToStrict(quotaEnforcerA);
     resetToStrict(quotaEnforcerB);
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
index a555ba4..a075690 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RestApiQuotaIT.java
@@ -107,7 +107,7 @@
     expect(quotaBackendWithResource.requestToken("/restapi/accounts/detail:GET"))
         .andReturn(singletonAggregation(QuotaResponse.ok()));
     replay(quotaBackendWithResource);
-    expect(quotaBackendWithUser.account(admin.id)).andReturn(quotaBackendWithResource);
+    expect(quotaBackendWithUser.account(admin.id())).andReturn(quotaBackendWithResource);
     replay(quotaBackendWithUser);
     adminRestSession.get("/accounts/self/detail").assertOK();
     verify(quotaBackendWithUser);
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
index df8c5af..83782c9 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
@@ -63,7 +63,7 @@
 
     // Create change as user
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(project, user);
-    PushOneCommit push = pushFactory.create(user.getIdent(), userTestRepo);
+    PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
 
     // Approve as admin
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
index 8fc32b4..fa13be4 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
@@ -87,17 +87,17 @@
     SubmitRecord.Label submitRecordLabel1 = new SubmitRecord.Label();
     submitRecordLabel1.label = "Verified";
     submitRecordLabel1.status = SubmitRecord.Label.Status.REJECT;
-    submitRecordLabel1.appliedBy = admin.id;
+    submitRecordLabel1.appliedBy = admin.id();
 
     SubmitRecord.Label submitRecordLabel2 = new SubmitRecord.Label();
     submitRecordLabel2.label = "Code-Review";
     submitRecordLabel2.status = SubmitRecord.Label.Status.OK;
-    submitRecordLabel2.appliedBy = admin.id;
+    submitRecordLabel2.appliedBy = admin.id();
 
     SubmitRecord.Label submitRecordLabel3 = new SubmitRecord.Label();
     submitRecordLabel3.label = "Any-Label-Name";
     submitRecordLabel3.status = SubmitRecord.Label.Status.REJECT;
-    submitRecordLabel3.appliedBy = user.id;
+    submitRecordLabel3.appliedBy = user.id();
 
     List<Term> terms = new ArrayList<>();
 
@@ -140,7 +140,7 @@
   }
 
   private static StructureTerm makeLabel(String name, String status, TestAccount account) {
-    StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id.get()));
+    StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id().get()));
     return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status, user));
   }
 
@@ -150,7 +150,7 @@
 
   private ChangeData makeChangeData() {
     ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
-    cd.setChange(TestChanges.newChange(project, admin.id));
+    cd.setChange(TestChanges.newChange(project, admin.id()));
     return cd;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index 53ac70b..c69712c 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -74,7 +74,7 @@
     modifySubmitRules(
         String.format(
             "gerrit:commit_author(user(%d), '%s', '%s')",
-            user.getId().get(), user.fullName, user.email));
+            user.id().get(), user.fullName(), user.email()));
     assertThat(statusForRule()).isEqualTo(SubmitRecord.Status.OK);
   }
 
@@ -87,7 +87,7 @@
   private SubmitRecord.Status statusForRule() throws Exception {
     String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
-        pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
+        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
     ChangeData cd = result1.getChange();
 
@@ -112,8 +112,8 @@
       testRepo
           .branch(RefNames.REFS_CONFIG)
           .commit()
-          .author(admin.getIdent())
-          .committer(admin.getIdent())
+          .author(admin.newIdent())
+          .committer(admin.newIdent())
           .add("rules.pl", newContent)
           .message("Modify rules.pl")
           .create();
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index e583179..0ce05b0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -49,4 +49,38 @@
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNull();
   }
+
+  @Test
+  public void withDotGit() throws Exception {
+    String newGroupName = "newGroup";
+    adminRestSession.put("/groups/" + newGroupName);
+    String newProjectName = name("newProject");
+    adminSshSession.exec(
+        "gerrit create-project --branch master --owner "
+            + newGroupName
+            + " "
+            + newProjectName
+            + ".git");
+    adminSshSession.assertSuccess();
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertThat(projectState.getName()).isEqualTo(newProjectName);
+  }
+
+  @Test
+  public void withTrailingSlash() throws Exception {
+    String newGroupName = "newGroup";
+    adminRestSession.put("/groups/" + newGroupName);
+    String newProjectName = name("newProject");
+    adminSshSession.exec(
+        "gerrit create-project --branch master --owner "
+            + newGroupName
+            + " "
+            + newProjectName
+            + "/");
+    adminSshSession.assertSuccess();
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertThat(projectState.getName()).isEqualTo(newProjectName);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 4e88955..7f2abc8 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -31,7 +31,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_5);
+    return getConfig(ElasticVersion.V6_7);
   }
 
   @ConfigSuite.Config
diff --git a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
new file mode 100644
index 0000000..e61e2cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.io.CharStreams;
+import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.OutputStreamQuery;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+@UseSsh
+public class PluginChangeFieldsIT extends AbstractPluginFieldsTest {
+  // No tests for getting a single change over SSH, since the only API is the query API.
+
+  private static final Gson GSON = OutputStreamQuery.GSON;
+
+  @Test
+  public void queryChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
+  }
+
+  @Test
+  public void queryChangeWithSimpleAttribute() throws Exception {
+    getChangeWithSimpleAttribute(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
+  }
+
+  @Test
+  public void queryChangeWithOption() throws Exception {
+    getChangeWithOption(
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id, opts))));
+  }
+
+  private String changeQueryCmd(Change.Id id) {
+    return changeQueryCmd(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryCmd(Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return "gerrit query --format json "
+        + pluginOptions.entries().stream()
+            .flatMap(e -> Stream.of("--" + e.getKey(), e.getValue()))
+            .collect(joining(" "))
+        + " "
+        + id;
+  }
+
+  @Nullable
+  private static List<MyInfo> pluginInfoFromSingletonList(String sshOutput) throws Exception {
+    List<Map<String, Object>> changeAttrs = new ArrayList<>();
+    for (String line : CharStreams.readLines(new StringReader(sshOutput))) {
+      Map<String, Object> changeAttr =
+          GSON.fromJson(line, new TypeToken<Map<String, Object>>() {}.getType());
+      if (!"stats".equals(changeAttr.get("type"))) {
+        changeAttrs.add(changeAttr);
+      }
+    }
+
+    assertThat(changeAttrs).hasSize(1);
+    return decodeRawPluginsList(GSON, changeAttrs.get(0).get("plugins"));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 50b2a78d..0f47a4a 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -84,7 +84,7 @@
   public void allReviewersOptionJSON() throws Exception {
     String changeId = createChange().getChangeId();
     AddReviewerInput in = new AddReviewerInput();
-    in.reviewer = user.email;
+    in.reviewer = user.email();
     gApi.changes().id(changeId).addReviewer(in);
 
     List<ChangeAttribute> changes = executeSuccessfulQuery(changeId);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 237859c..bb5b7c9 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;
 
@@ -50,13 +50,13 @@
 
   private void setReviewer(boolean add, String id) throws Exception {
     adminSshSession.exec(
-        String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email, id));
+        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);
+      assertThat(reviewers).contains(user.id());
     } else {
-      assertThat(reviewers).doesNotContain(user.id);
+      assertThat(reviewers).doesNotContain(user.id());
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/ssh/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/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index 954b0e6..e0d6593 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.truth.Correspondence;
+import com.google.common.truth.Correspondence.BinaryPredicate;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -31,14 +32,10 @@
 import java.sql.Timestamp;
 import java.util.Objects;
 import java.util.Optional;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class GroupOperationsImplTest extends AbstractDaemonTest {
 
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
   @Inject private AccountOperations accountOperations;
 
   @Inject private GroupOperationsImpl groupOperations;
@@ -231,7 +228,7 @@
   public void retrievingNotExistingGroupFails() throws Exception {
     AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
 
-    expectedException.expect(IllegalStateException.class);
+    exception.expect(IllegalStateException.class);
     groupOperations.group(notExistingGroupUuid).get();
   }
 
@@ -617,40 +614,34 @@
   }
 
   private static Correspondence<AccountInfo, Account.Id> getAccountToIdCorrespondence() {
-    return new Correspondence<AccountInfo, Account.Id>() {
-      @Override
-      public boolean compare(AccountInfo actualAccount, Account.Id expectedId) {
-        Account.Id accountId =
-            Optional.ofNullable(actualAccount)
-                .map(account -> account._accountId)
-                .map(Account.Id::new)
-                .orElse(null);
-        return Objects.equals(accountId, expectedId);
-      }
-
-      @Override
-      public String toString() {
-        return "has ID";
-      }
-    };
+    return Correspondence.from(
+        new BinaryPredicate<AccountInfo, Account.Id>() {
+          @Override
+          public boolean apply(AccountInfo actualAccount, Account.Id expectedId) {
+            Account.Id accountId =
+                Optional.ofNullable(actualAccount)
+                    .map(account -> account._accountId)
+                    .map(Account.Id::new)
+                    .orElse(null);
+            return Objects.equals(accountId, expectedId);
+          }
+        },
+        "has ID");
   }
 
   private static Correspondence<GroupInfo, AccountGroup.UUID> getGroupToUuidCorrespondence() {
-    return new Correspondence<GroupInfo, AccountGroup.UUID>() {
-      @Override
-      public boolean compare(GroupInfo actualGroup, AccountGroup.UUID expectedUuid) {
-        AccountGroup.UUID groupUuid =
-            Optional.ofNullable(actualGroup)
-                .map(group -> group.id)
-                .map(AccountGroup.UUID::new)
-                .orElse(null);
-        return Objects.equals(groupUuid, expectedUuid);
-      }
-
-      @Override
-      public String toString() {
-        return "has UUID";
-      }
-    };
+    return Correspondence.from(
+        new BinaryPredicate<GroupInfo, AccountGroup.UUID>() {
+          @Override
+          public boolean apply(GroupInfo actualGroup, AccountGroup.UUID expectedUuid) {
+            AccountGroup.UUID groupUuid =
+                Optional.ofNullable(actualGroup)
+                    .map(group -> group.id)
+                    .map(AccountGroup.UUID::new)
+                    .orElse(null);
+            return Objects.equals(groupUuid, expectedUuid);
+          }
+        },
+        "has UUID");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
index 9b498011..5cbed1b 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.CurrentUser.PropertyKey;
-import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -48,50 +48,50 @@
 
   @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());
+    fastCheckCurrentUser(admin.id());
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.id());
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
+    checkCurrentUser(user.id());
   }
 
   @Test
   public void setApiUserToExistingUserByTestAccount() throws Exception {
-    fastCheckCurrentUser(admin.getId());
+    fastCheckCurrentUser(admin.id());
     TestAccount testAccount =
         accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
     AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
-    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
     checkCurrentUser(testAccount.accountId());
   }
 
   @Test
   public void setApiUserToNonExistingUser() throws Exception {
-    fastCheckCurrentUser(admin.getId());
+    fastCheckCurrentUser(admin.id());
     try {
       requestScopeOperations.setApiUser(new Account.Id(sequences.nextAccountId()));
       assert_().fail("expected RuntimeException");
     } catch (RuntimeException e) {
       // Expected.
     }
-    checkCurrentUser(admin.getId());
+    checkCurrentUser(admin.id());
   }
 
   @Test
   public void resetCurrentApiUserClearsCachedState() throws Exception {
-    requestScopeOperations.setApiUser(user.getId());
+    requestScopeOperations.setApiUser(user.id());
     PropertyKey<String> key = PropertyKey.create();
     atrScope.get().getUser().put(key, "foo");
     assertThat(atrScope.get().getUser().get(key)).hasValue("foo");
 
     AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.resetCurrentApiUser();
-    checkCurrentUser(user.getId());
+    checkCurrentUser(user.id());
     assertThat(atrScope.get().getUser().get(key)).isEmpty();
     assertThat(oldCtx.getUser().get(key)).hasValue("foo");
   }
 
   @Test
   public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
-    fastCheckCurrentUser(admin.getId());
+    fastCheckCurrentUser(admin.id());
     requestScopeOperations.setApiUserAnonymous();
     assertThat(userProvider.get()).isInstanceOf(AnonymousUser.class);
   }
diff --git a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
index ef71b67..a534a9e 100644
--- a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
@@ -21,7 +21,6 @@
 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;
@@ -103,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/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index f9c867b..a2bd092 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -65,7 +65,7 @@
     name = "elasticsearch_query_%ss_test_V6" % name,
     size = "large",
     srcs = [src],
-    tags = ELASTICSEARCH_TAGS + ["flaky"],
+    tags = ELASTICSEARCH_TAGS,
     deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
 ) for name, src in ELASTICSEARCH_TESTS_V6.items()]
 
@@ -73,7 +73,7 @@
     name = "elasticsearch_query_%ss_test_V7" % name,
     size = "large",
     srcs = [src],
-    tags = ELASTICSEARCH_TAGS + ["flaky"],
+    tags = ELASTICSEARCH_TAGS,
     deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name] + [
         "//lib/httpcomponents:httpasyncclient",
         "//lib/httpcomponents:httpclient",
@@ -93,6 +93,7 @@
         "//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 735354e..9ce1456 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.elasticsearch.ElasticConfiguration.DEFAULT_MAX_RETRY_TIMEOUT_MS;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.DEFAULT_USERNAME;
-import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_MAX_RETRY_TIMEOUT;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_PASSWORD;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_PREFIX;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_SERVER;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.KEY_USERNAME;
-import static com.google.gerrit.elasticsearch.ElasticConfiguration.MAX_RETRY_TIMEOUT_UNIT;
 import static com.google.gerrit.elasticsearch.ElasticConfiguration.SECTION_ELASTICSEARCH;
 import static java.util.stream.Collectors.toList;
 
@@ -30,7 +27,7 @@
 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.Test;
 
@@ -43,7 +40,6 @@
     assertThat(esCfg.username).isNull();
     assertThat(esCfg.password).isNull();
     assertThat(esCfg.prefix).isEmpty();
-    assertThat(esCfg.maxRetryTimeout).isEqualTo(DEFAULT_MAX_RETRY_TIMEOUT_MS);
   }
 
   @Test
@@ -63,23 +59,6 @@
   }
 
   @Test
-  public void maxRetryTimeoutInDefaultUnit() {
-    Config cfg = newConfig();
-    cfg.setString(SECTION_ELASTICSEARCH, null, KEY_MAX_RETRY_TIMEOUT, "45000");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
-    assertThat(esCfg.maxRetryTimeout).isEqualTo(45000);
-  }
-
-  @Test
-  public void maxRetryTimeoutInOtherUnit() {
-    Config cfg = newConfig();
-    cfg.setString(SECTION_ELASTICSEARCH, null, KEY_MAX_RETRY_TIMEOUT, "45 s");
-    ElasticConfiguration esCfg = new ElasticConfiguration(cfg);
-    assertThat(esCfg.maxRetryTimeout)
-        .isEqualTo(MAX_RETRY_TIMEOUT_UNIT.convert(45, TimeUnit.SECONDS));
-  }
-
-  @Test
   public void withAuthentication() throws Exception {
     Config cfg = newConfig();
     cfg.setString(SECTION_ELASTICSEARCH, null, KEY_USERNAME, "myself");
@@ -138,7 +117,7 @@
   }
 
   private void assertHosts(ElasticConfiguration cfg, Object... hostURIs) throws Exception {
-    assertThat(Arrays.asList(cfg.getHosts()).stream().map(h -> h.toURI()).collect(toList()))
+    assertThat(Arrays.asList(cfg.getHosts()).stream().map(HttpHost::toURI).collect(toList()))
         .containsExactly(hostURIs);
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 8583d2d..8400d02 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -37,7 +37,7 @@
   private static String getImageName(ElasticVersion version) {
     switch (version) {
       case V5_6:
-        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.14";
+        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.16";
       case V6_2:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4";
       case V6_3:
@@ -46,8 +46,12 @@
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3";
       case V6_5:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.4";
+      case V6_6:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.2";
+      case V6_7:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.7.1";
       case V7_0:
-        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-alpha2";
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 020a158..9aaf4bb 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -19,7 +19,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
@@ -48,7 +47,7 @@
     configure(config, port, prefix, null);
   }
 
-  public static void createAllIndexes(Injector injector) throws IOException {
+  public static void createAllIndexes(Injector injector) {
     Collection<IndexDefinition<?, ?, ?>> indexDefs =
         injector.getInstance(Key.get(new TypeLiteral<Collection<IndexDefinition<?, ?, ?>>>() {}));
     for (IndexDefinition<?, ?, ?> indexDef : indexDefs) {
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index 53593ef..219eecd 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_7);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 6429431..1b0822c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_7);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index de0af97..2782b7f 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_7);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index 0ce66e8..f01138a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_7);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index baf6c2b..61ad068 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -37,6 +37,12 @@
     assertThat(ElasticVersion.forVersion("6.5.0")).isEqualTo(ElasticVersion.V6_5);
     assertThat(ElasticVersion.forVersion("6.5.1")).isEqualTo(ElasticVersion.V6_5);
 
+    assertThat(ElasticVersion.forVersion("6.6.0")).isEqualTo(ElasticVersion.V6_6);
+    assertThat(ElasticVersion.forVersion("6.6.1")).isEqualTo(ElasticVersion.V6_6);
+
+    assertThat(ElasticVersion.forVersion("6.7.0")).isEqualTo(ElasticVersion.V6_7);
+    assertThat(ElasticVersion.forVersion("6.7.1")).isEqualTo(ElasticVersion.V6_7);
+
     assertThat(ElasticVersion.forVersion("7.0.0")).isEqualTo(ElasticVersion.V7_0);
     assertThat(ElasticVersion.forVersion("7.0.1")).isEqualTo(ElasticVersion.V7_0);
   }
@@ -55,6 +61,8 @@
     assertThat(ElasticVersion.V6_2.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_3.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_4.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V6_5.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V6_6.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_0.isV6OrLater()).isTrue();
   }
 
@@ -64,6 +72,8 @@
     assertThat(ElasticVersion.V6_2.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_3.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_4.isV7OrLater()).isFalse();
+    assertThat(ElasticVersion.V6_5.isV7OrLater()).isFalse();
+    assertThat(ElasticVersion.V6_6.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V7_0.isV7OrLater()).isTrue();
   }
 }
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index adf696d..94e433c 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -8,6 +8,7 @@
         "//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/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/gpg/BUILD b/javatests/com/google/gerrit/gpg/BUILD
index baf65b7..6edfa93 100644
--- a/javatests/com/google/gerrit/gpg/BUILD
+++ b/javatests/com/google/gerrit/gpg/BUILD
@@ -19,7 +19,6 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/bouncycastle:bcpg",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov",
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
index 95c0e85..be65752 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithExpiration;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpirationWithSubkeyWithExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -101,6 +102,25 @@
   }
 
   @Test
+  public void getSubkeyReturnsMasterKey() throws Exception {
+    TestKey key1 = validKeyWithoutExpirationWithSubkeyWithExpiration();
+    PGPPublicKeyRing keyRing = key1.getPublicKeyRing();
+    store.add(keyRing);
+
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    long masterKeyId = key1.getKeyId();
+    long subKeyId = 0;
+    for (PGPPublicKey key : keyRing) {
+      if (masterKeyId != subKeyId) {
+        subKeyId = key.getKeyID();
+      }
+    }
+
+    assertKeys(subKeyId, key1);
+  }
+
+  @Test
   public void getMultiple() throws Exception {
     TestKey key1 = validKeyWithoutExpiration();
     TestKey key2 = validKeyWithExpiration();
@@ -202,6 +222,29 @@
   }
 
   @Test
+  public void removeMasterKeyRemovesSubkey() throws Exception {
+    TestKey key1 = validKeyWithoutExpirationWithSubkeyWithExpiration();
+    PGPPublicKeyRing keyRing = key1.getPublicKeyRing();
+    store.add(keyRing);
+
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    long masterKeyId = key1.getKeyId();
+    long subKeyId = 0;
+    for (PGPPublicKey key : keyRing) {
+      if (masterKeyId != subKeyId) {
+        subKeyId = key.getKeyID();
+      }
+    }
+
+    store.remove(key1.getPublicKey().getFingerprint());
+    assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
+
+    assertKeys(masterKeyId);
+    assertKeys(subKeyId);
+  }
+
+  @Test
   public void removeNonexisting() throws Exception {
     TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index fe76d54..0fbd922 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -15,7 +15,6 @@
         "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:jimfs",
         "//lib:junit",
         "//lib:servlet-api-3_1-without-neverlink",
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index e3436bc..a1f60de 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/index/query/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:junit",
diff --git a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
new file mode 100644
index 0000000..6a397dc
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index.query;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.util.Collection;
+import java.util.Objects;
+import org.junit.Test;
+
+public class QueryBuilderTest extends GerritBaseTests {
+  private static class TestPredicate extends Predicate<Object> {
+    private final String field;
+    private final String value;
+
+    TestPredicate(String field, String value) {
+      this.field = field;
+      this.value = value;
+    }
+
+    @Override
+    public Predicate<Object> copy(Collection<? extends Predicate<Object>> children) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(field, value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof TestPredicate)) {
+        return false;
+      }
+      TestPredicate p = (TestPredicate) o;
+      return Objects.equals(field, p.field) && Objects.equals(value, p.value);
+    }
+  }
+
+  private static class TestQueryBuilder extends QueryBuilder<Object, TestQueryBuilder> {
+    TestQueryBuilder() {
+      super(new QueryBuilder.Definition<>(TestQueryBuilder.class), null);
+    }
+
+    @Operator
+    public Predicate<Object> a(String value) {
+      return new TestPredicate("a", value);
+    }
+  }
+
+  @Test
+  public void fieldNameAndValue() throws Exception {
+    assertThat(parse("a:foo")).isEqualTo(new TestPredicate("a", "foo"));
+  }
+
+  @Test
+  public void fieldWithParenthesizedValues() throws Exception {
+    assertThatParseException("a:(foo bar)").hasMessageThat().contains("no viable alternative");
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    assertThat(parse("a:foo:bar")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    assertThat(parse("a:*:bar")).isEqualTo(new TestPredicate("a", "*:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    assertThat(parse("a:*:*:*")).isEqualTo(new TestPredicate("a", "*:*:*"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotes() throws Exception {
+    assertThat(parse("a:\"foo bar\"")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotesAndColon() throws Exception {
+    assertThat(parse("a:\"foo:bar\"")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBraces() throws Exception {
+    assertThat(parse("a:{foo bar}")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBracesAndColon() throws Exception {
+    assertThat(parse("a:{foo:bar}")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  private static Predicate<Object> parse(String query) throws Exception {
+    return new TestQueryBuilder().parse(query);
+  }
+
+  private static ThrowableSubject assertThatParseException(String query) {
+    try {
+      new TestQueryBuilder().parse(query);
+      throw new AssertionError("expected QueryParseException for " + query);
+    } catch (QueryParseException e) {
+      return assertThat(e);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index 2175f7d..b4dd1ee 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -14,7 +14,14 @@
 
 package com.google.gerrit.index.query;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.index.query.QueryParser.AND;
+import static com.google.gerrit.index.query.QueryParser.COLON;
+import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
+import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
+import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
+import static com.google.gerrit.index.query.QueryParser.parse;
+import static com.google.gerrit.index.query.testing.TreeSubject.assertThat;
 
 import com.google.gerrit.testing.GerritBaseTests;
 import org.antlr.runtime.tree.Tree;
@@ -22,27 +29,188 @@
 
 public class QueryParserTest extends GerritBaseTests {
   @Test
-  public void projectBare() throws QueryParseException {
-    Tree r;
-
-    r = parse("project:tools/gerrit");
-    assertSingleWord("project", "tools/gerrit", r);
-
-    r = parse("project:tools/*");
-    assertSingleWord("project", "tools/*", r);
+  public void fieldNameAndValue() throws Exception {
+    Tree r = parse("project:tools/gerrit");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(1);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("tools/gerrit");
+    assertThat(r).child(0).hasNoChildren();
   }
 
-  private static void assertSingleWord(String name, String value, Tree r) {
-    assertEquals(QueryParser.FIELD_NAME, r.getType());
-    assertEquals(name, r.getText());
-    assertEquals(1, r.getChildCount());
-    final Tree c = r.getChild(0);
-    assertEquals(QueryParser.SINGLE_WORD, c.getType());
-    assertEquals(value, c.getText());
-    assertEquals(0, c.getChildCount());
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:foo:");
   }
 
-  private static Tree parse(String str) throws QueryParseException {
-    return QueryParser.parse(str);
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    Tree r = parse("project:foo:bar");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("foo");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("bar");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    Tree r = parse("project:x*y:a*b");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("x*y");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("a*b");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:x*y:");
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    Tree r = parse("project:*:*:*");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(5);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("*");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("*");
+    assertThat(r).child(2).hasNoChildren();
+    assertThat(r).child(3).hasType(COLON);
+    assertThat(r).child(3).hasText(":");
+    assertThat(r).child(3).hasNoChildren();
+    assertThat(r).child(4).hasType(SINGLE_WORD);
+    assertThat(r).child(4).hasText("*");
+    assertThat(r).child(4).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByAnotherField() throws Exception {
+    Tree r = parse("project:foo:bar file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByOpenParen() throws Exception {
+    Tree r = parse("project:foo:bar (file:baz)");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByCloseParen() throws Exception {
+    Tree r = parse("(project:foo:bar) file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void defaultFieldWithColon() throws Exception {
+    Tree r = parse("CodeReview:+2");
+    assertThat(r).hasType(DEFAULT_FIELD);
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("CodeReview");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("+2");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  private static void assertParseFails(String query) {
+    try {
+      parse(query);
+      assert_().fail("expected parse to fail: %s", query);
+    } catch (QueryParseException e) {
+      // Expected.
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD
index 2fd8f24..5cfa00c 100644
--- a/javatests/com/google/gerrit/mail/BUILD
+++ b/javatests/com/google/gerrit/mail/BUILD
@@ -24,7 +24,6 @@
         "//java/org/eclipse/jgit:server",
         "//lib:gson",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib/commons:codec",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java b/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
new file mode 100644
index 0000000..b269f11
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/AccountGroupTest.java
@@ -0,0 +1,102 @@
+// 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();
+  }
+
+  @Test
+  public void uuidToString() {
+    assertThat(uuid("foo").toString()).isEqualTo("foo");
+    assertThat(uuid("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(uuid("foo:bar").toString()).isEqualTo("foo%3Abar");
+  }
+
+  @Test
+  public void parseUuid() {
+    assertThat(AccountGroup.UUID.parse("foo")).isEqualTo(new AccountGroup.UUID("foo"));
+    assertThat(AccountGroup.UUID.parse("foo+bar")).isEqualTo(new AccountGroup.UUID("foo bar"));
+    assertThat(AccountGroup.UUID.parse("foo%3Abar")).isEqualTo(new AccountGroup.UUID("foo:bar"));
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(new AccountGroup.Id(123).toString()).isEqualTo("123");
+  }
+
+  @Test
+  public void nameKeyToString() {
+    assertThat(new AccountGroup.NameKey("foo").toString()).isEqualTo("foo");
+    assertThat(new AccountGroup.NameKey("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(new AccountGroup.NameKey("foo:bar").toString()).isEqualTo("foo%3Abar");
+  }
+
+  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
new file mode 100644
index 0000000..11a562f
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/AccountTest.java
@@ -0,0 +1,97 @@
+// 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/BUILD b/javatests/com/google/gerrit/reviewdb/client/BUILD
new file mode 100644
index 0000000..391d80e
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/BUILD
@@ -0,0 +1,14 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "client_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server/project/testing:project-test-util",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/reviewdb/client/BranchTest.java b/javatests/com/google/gerrit/reviewdb/client/BranchTest.java
new file mode 100644
index 0000000..7b0c31d
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/BranchTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class BranchTest {
+  @Test
+  public void canonicalizeNameDuringConstruction() {
+    assertThat(new Branch.NameKey(new Project.NameKey("foo"), "bar").get())
+        .isEqualTo("refs/heads/bar");
+    assertThat(new Branch.NameKey(new Project.NameKey("foo"), "refs/heads/bar").get())
+        .isEqualTo("refs/heads/bar");
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(new Branch.NameKey(new Project.NameKey("foo"), "bar").toString())
+        .isEqualTo("foo,refs/heads/bar");
+    assertThat(new Branch.NameKey(new Project.NameKey("foo bar"), "bar baz").toString())
+        .isEqualTo("foo+bar,refs/heads/bar+baz");
+    assertThat(new Branch.NameKey(new Project.NameKey("foo^bar"), "bar^baz").toString())
+        .isEqualTo("foo%5Ebar,refs/heads/bar%5Ebaz");
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java b/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
new file mode 100644
index 0000000..724a98a
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -0,0 +1,186 @@
+// 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.eclipse.jgit.lib.ObjectId;
+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");
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(new Change.Id(3).toString()).isEqualTo("3");
+  }
+
+  @Test
+  public void keyToString() {
+    String key = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    assertThat(ObjectId.isId(key.substring(1))).isTrue();
+    assertThat(new Change.Key(key).toString()).isEqualTo(key);
+  }
+
+  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
new file mode 100644
index 0000000..5e42ce0
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
@@ -0,0 +1,53 @@
+// 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
new file mode 100644
index 0000000..90a2120
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -0,0 +1,129 @@
+// 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.common.truth.Truth.assert_;
+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");
+  }
+
+  @Test
+  public void parseId() {
+    assertThat(PatchSet.Id.parse("1,2")).isEqualTo(new PatchSet.Id(new Change.Id(1), 2));
+    assertThat(PatchSet.Id.parse("01,02")).isEqualTo(new PatchSet.Id(new Change.Id(1), 2));
+    assertInvalidId(null);
+    assertInvalidId("");
+    assertInvalidId("1");
+    assertInvalidId("1,foo.txt");
+    assertInvalidId("foo.txt,1");
+
+    String hexComma = "%" + String.format("%02x", (int) ',');
+    assertInvalidId("1" + hexComma + "2");
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(new PatchSet.Id(new Change.Id(2), 3).toString()).isEqualTo("2,3");
+  }
+
+  private static void assertRef(int changeId, int psId, String refName) {
+    assertThat(PatchSet.isChangeRef(refName)).isTrue();
+    assertThat(PatchSet.Id.fromRef(refName))
+        .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
+  }
+
+  private static void assertNotRef(String refName) {
+    assertThat(PatchSet.isChangeRef(refName)).isFalse();
+    assertThat(PatchSet.Id.fromRef(refName)).isNull();
+  }
+
+  private static void assertInvalidId(String str) {
+    try {
+      PatchSet.Id.parse(str);
+      assert_().fail("expected RuntimeException");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchTest.java b/javatests/com/google/gerrit/reviewdb/client/PatchTest.java
new file mode 100644
index 0000000..fcb940d
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/PatchTest.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class PatchTest extends GerritBaseTests {
+  @Test
+  public void isMagic() {
+    assertThat(Patch.isMagic("/COMMIT_MSG")).isTrue();
+    assertThat(Patch.isMagic("/MERGE_LIST")).isTrue();
+
+    assertThat(Patch.isMagic("/COMMIT_MSG/")).isFalse();
+    assertThat(Patch.isMagic("COMMIT_MSG")).isFalse();
+    assertThat(Patch.isMagic("/commit_msg")).isFalse();
+  }
+
+  @Test
+  public void parseKey() {
+    assertThat(Patch.Key.parse("1,2,foo.txt"))
+        .isEqualTo(new Patch.Key(new PatchSet.Id(new Change.Id(1), 2), "foo.txt"));
+    assertThat(Patch.Key.parse("01,02,foo.txt"))
+        .isEqualTo(new Patch.Key(new PatchSet.Id(new Change.Id(1), 2), "foo.txt"));
+    assertInvalidKey(null);
+    assertInvalidKey("");
+    assertInvalidKey("1,2");
+    assertInvalidKey("1, 2, foo.txt");
+    assertInvalidKey("1,foo.txt");
+    assertInvalidKey("1,foo.txt,2");
+    assertInvalidKey("foo.txt,1,2");
+
+    String hexComma = "%" + String.format("%02x", (int) ',');
+    assertInvalidKey("1" + hexComma + "2" + hexComma + "foo.txt");
+  }
+
+  private static void assertInvalidKey(String str) {
+    try {
+      Patch.Key.parse(str);
+      assert_().fail("expected RuntimeException");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/ProjectTest.java b/javatests/com/google/gerrit/reviewdb/client/ProjectTest.java
new file mode 100644
index 0000000..537df5f
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/ProjectTest.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.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class ProjectTest extends GerritBaseTests {
+  @Test
+  public void parseId() {
+    assertThat(Project.NameKey.parse("foo")).isEqualTo(new Project.NameKey("foo"));
+    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(new Project.NameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(new Project.NameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(new Project.NameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(new Project.NameKey("foo/bar"));
+  }
+
+  @Test
+  public void idToString() {
+    assertThat(new Project.NameKey("foo").toString()).isEqualTo("foo");
+    assertThat(new Project.NameKey("foo bar").toString()).isEqualTo("foo+bar");
+    assertThat(new Project.NameKey("foo/bar").toString()).isEqualTo("foo/bar");
+    assertThat(new Project.NameKey("foo^bar").toString()).isEqualTo("foo%5Ebar");
+    assertThat(new Project.NameKey("foo%bar").toString()).isEqualTo("foo%25bar");
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java b/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
new file mode 100644
index 0000000..fa6a722
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -0,0 +1,300 @@
+// 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/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
index 30487a6..61bf105 100644
--- a/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeProtoConverterTest.java
@@ -253,7 +253,7 @@
     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.getStatus()).isEqualTo(Change.Status.NEW);
+    assertThat(change.isNew()).isTrue();
     assertThat(change.isPrivate()).isFalse();
     assertThat(change.isWorkInProgress()).isFalse();
     assertThat(change.hasReviewStarted()).isFalse();
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 5400ed1..321d1fd 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -36,6 +36,7 @@
         ":custom-truth-subjects",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//java/com/google/gerrit/git",
@@ -56,6 +57,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",
@@ -63,7 +65,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index a8daac3..485de49 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -25,7 +25,7 @@
 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;
@@ -81,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 fff8a86..51a34f5 100644
--- a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -141,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/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index e4f8ba8..3c7e492 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.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.server.auth.oauth;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
index ddad4b9..a0d5ea6 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -7,8 +7,8 @@
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gwtorm",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
@@ -17,5 +17,6 @@
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
+        "//proto/testing:test_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
similarity index 62%
rename from javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
index 56dd6ad..14ba779 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
@@ -17,42 +17,31 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Converter;
 import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gwtorm.client.IntKey;
-import com.google.gwtorm.client.Key;
 import org.junit.Test;
 
-public class IntKeyCacheSerializerTest extends GerritBaseTests {
-
-  private static class MyIntKey extends IntKey<Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    private int val;
-
-    MyIntKey(int val) {
-      this.val = val;
+public class CacheSerializerTest extends GerritBaseTests {
+  @AutoValue
+  abstract static class MyAutoValue {
+    static MyAutoValue create(int val) {
+      return new AutoValue_CacheSerializerTest_MyAutoValue(val);
     }
 
-    @Override
-    public int get() {
-      return val;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      this.val = newValue;
-    }
+    abstract int val();
   }
 
-  private static final IntKeyCacheSerializer<MyIntKey> SERIALIZER =
-      new IntKeyCacheSerializer<>(MyIntKey::new);
+  private static final CacheSerializer<MyAutoValue> SERIALIZER =
+      CacheSerializer.convert(
+          IntegerCacheSerializer.INSTANCE, Converter.from(MyAutoValue::val, MyAutoValue::create));
 
   @Test
   public void serialize() throws Exception {
-    MyIntKey k = new MyIntKey(1234);
-    byte[] serialized = SERIALIZER.serialize(k);
+    MyAutoValue v = MyAutoValue.create(1234);
+    byte[] serialized = SERIALIZER.serialize(v);
     assertThat(serialized).isEqualTo(new byte[] {-46, 9});
-    assertThat(SERIALIZER.deserialize(serialized).get()).isEqualTo(1234);
+    assertThat(SERIALIZER.deserialize(serialized).val()).isEqualTo(1234);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
new file mode 100644
index 0000000..845da9b
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.proto.testing.Test.SerializableProto;
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class ProtobufSerializerTest extends GerritBaseTests {
+  @Test
+  public void requiredAndOptionalTypes() {
+    assertRoundTrip(SerializableProto.newBuilder().setId(123));
+    assertRoundTrip(SerializableProto.newBuilder().setId(123).setText("foo bar"));
+  }
+
+  @Test
+  public void exactByteSequence() {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    SerializableProto proto = SerializableProto.newBuilder().setId(123).setText("foo bar").build();
+    byte[] serialized = s.serialize(proto);
+    // Hard-code byte sequence to detect library changes
+    assertThat(serialized).isEqualTo(new byte[] {8, 123, 18, 7, 102, 111, 111, 32, 98, 97, 114});
+  }
+
+  private static void assertRoundTrip(SerializableProto.Builder input) {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    assertThat(s.deserialize(s.serialize(input.build()))).isEqualTo(input.build());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java b/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java
new file mode 100644
index 0000000..e996976
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/AllProjectsNameTest.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Project;
+import org.junit.Test;
+
+public class AllProjectsNameTest {
+  @Test
+  public void equalToProjectNameKey() {
+    String name = "a-project";
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    Project.NameKey projectName = new Project.NameKey(name);
+    assertThat(allProjectsName.get()).isEqualTo(projectName.get());
+    assertThat(allProjectsName).isEqualTo(projectName);
+  }
+
+  @Test
+  public void equalToAllUsersName() {
+    String name = "a-project";
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    AllUsersName allUsersName = new AllUsersName(name);
+    assertThat(allProjectsName.get()).isEqualTo(allUsersName.get());
+    assertThat(allProjectsName).isEqualTo(allUsersName);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/AllUsersNameTest.java b/javatests/com/google/gerrit/server/config/AllUsersNameTest.java
new file mode 100644
index 0000000..a4d56cd
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/AllUsersNameTest.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Project;
+import org.junit.Test;
+
+public class AllUsersNameTest {
+  @Test
+  public void equalToProjectNameKey() {
+    String name = "a-project";
+    AllUsersName allUsersName = new AllUsersName(name);
+    Project.NameKey projectName = new Project.NameKey(name);
+    assertThat(allUsersName.get()).isEqualTo(projectName.get());
+    assertThat(allUsersName).isEqualTo(projectName);
+  }
+
+  @Test
+  public void equalToAllProjectsName() {
+    String name = "a-project";
+    AllUsersName allUsersName = new AllUsersName(name);
+    AllProjectsName allProjectsName = new AllProjectsName(name);
+    assertThat(allUsersName.get()).isEqualTo(allProjectsName.get());
+    assertThat(allUsersName).isEqualTo(allProjectsName);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index a4a196b..30fabdc 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -16,11 +16,13 @@
 
 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;
@@ -44,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
@@ -69,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
@@ -88,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/ScheduleConfigTest.java b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
index f0e9153..6926052 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -41,15 +41,18 @@
   @Test
   public void initialDelay() throws Exception {
     assertThat(initialDelay("11:00", "1h")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelay("11:00", "1 hour")).isEqualTo(ms(1, HOURS));
     assertThat(initialDelay("05:30", "1h")).isEqualTo(ms(30, MINUTES));
     assertThat(initialDelay("09:30", "1h")).isEqualTo(ms(30, MINUTES));
     assertThat(initialDelay("13:30", "1h")).isEqualTo(ms(30, MINUTES));
     assertThat(initialDelay("13:59", "1h")).isEqualTo(ms(59, MINUTES));
 
     assertThat(initialDelay("11:00", "1d")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelay("11:00", "1 day")).isEqualTo(ms(1, HOURS));
     assertThat(initialDelay("05:30", "1d")).isEqualTo(ms(19, HOURS) + ms(30, MINUTES));
 
     assertThat(initialDelay("11:00", "1w")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelay("11:00", "1 week")).isEqualTo(ms(1, HOURS));
     assertThat(initialDelay("05:30", "1w")).isEqualTo(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES));
 
     assertThat(initialDelay("Mon 11:00", "1w")).isEqualTo(ms(3, DAYS) + ms(1, HOURS));
@@ -200,6 +203,9 @@
 
     rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "0100");
     assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+    rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "1:00");
+    assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
index 574c795..265b24e 100644
--- a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.restapi.RawInput;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -41,16 +40,16 @@
 
   public StringSubject filePath() {
     isNotNull();
-    return Truth.assertThat(actual().getFilePath()).named("filePath");
+    return check("filePath()").that(actual().getFilePath());
   }
 
   public StringSubject newContent() throws IOException {
     isNotNull();
     RawInput newContent = actual().getNewContent();
-    Truth.assertThat(newContent).named("newContent").isNotNull();
+    check("newContent()").that(newContent).isNotNull();
     String contentString =
         CharStreams.toString(
             new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8));
-    return Truth.assertThat(contentString).named("newContent");
+    return check("newContent()").that(contentString);
   }
 }
diff --git a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
index 59ee2b7..bd9d4df 100644
--- a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -24,12 +24,17 @@
 public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> {
 
   public static TreeModificationSubject assertThat(TreeModification treeModification) {
-    return assertAbout(TreeModificationSubject::new).that(treeModification);
+    return assertAbout(treeModifications()).that(treeModification);
+  }
+
+  private static Factory<TreeModificationSubject, TreeModification> treeModifications() {
+    return TreeModificationSubject::new;
   }
 
   public static ListSubject<TreeModificationSubject, TreeModification> assertThatList(
       List<TreeModification> treeModifications) {
-    return ListSubject.assertThat(treeModifications, TreeModificationSubject::assertThat)
+    return assertAbout(ListSubject.elements())
+        .thatCustom(treeModifications, treeModifications())
         .named("treeModifications");
   }
 
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index 6c5c5b0..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;
@@ -55,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/git/PureRevertCacheKeyTest.java b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
new file mode 100644
index 0000000..8c17075
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.proto.Cache;
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class PureRevertCacheKeyTest {
+  @Test
+  public void serialization() {
+    ObjectId revert = ObjectId.zeroId();
+    ObjectId original = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+
+    byte[] serializedRevert =
+        new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    byte[] serializedOriginal =
+        byteArray(
+            0xaa, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+            0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb);
+
+    Cache.PureRevertKeyProto key =
+        PureRevertCache.key(new Project.NameKey("test"), revert, original);
+    assertThat(key)
+        .isEqualTo(
+            Cache.PureRevertKeyProto.newBuilder()
+                .setProject("test")
+                .setClaimedRevert(ByteString.copyFrom(serializedRevert))
+                .setClaimedOriginal(ByteString.copyFrom(serializedOriginal))
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index d02fa1b..b4652c9 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/common/data/testing:common-data-test-util",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//java/com/google/gerrit/git",
@@ -19,7 +20,6 @@
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 6f43380..c9ba72e 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static org.hamcrest.CoreMatchers.instanceOf;
 
@@ -1673,6 +1674,6 @@
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> loadedGroup) {
-    return assertThat(loadedGroup, InternalGroupSubject::assertThat);
+    return assertThat(loadedGroup, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index cff8189..e1211d3 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.common.data.testing.GroupReferenceSubject.groupReferences;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -25,6 +27,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.testing.GroupReferenceSubject;
+import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.testing.CommitInfoSubject;
 import com.google.gerrit.git.RefUpdateUtil;
@@ -41,7 +44,6 @@
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gerrit.truth.ListSubject;
 import com.google.gerrit.truth.OptionalSubject;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -127,7 +129,7 @@
     createGroup(groupUuid, groupName);
 
     AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("AnotherGroup");
-    exception.expect(OrmDuplicateKeyException.class);
+    exception.expect(DuplicateKeyException.class);
     exception.expectMessage(groupName.get());
     GroupNameNotes.forNewGroup(allUsersName, repo, anotherGroupUuid, groupName);
   }
@@ -203,7 +205,7 @@
     AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
     createGroup(anotherGroupUuid, anotherGroupName);
 
-    exception.expect(OrmDuplicateKeyException.class);
+    exception.expect(DuplicateKeyException.class);
     exception.expectMessage(anotherGroupName.get());
     GroupNameNotes.forRename(allUsersName, repo, groupUuid, groupName, anotherGroupName);
   }
@@ -251,7 +253,7 @@
     createGroup(anotherGroupUuid, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterFurtherGroup = log();
-    assertThatCommits(commitsAfterFurtherGroup).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterFurtherGroup).containsAtLeastElementsIn(commitsAfterCreation);
     assertThatCommits(commitsAfterFurtherGroup).lastElement().isNotIn(commitsAfterCreation);
   }
 
@@ -264,7 +266,7 @@
     renameGroup(groupUuid, groupName, anotherName);
 
     ImmutableList<CommitInfo> commitsAfterRename = log();
-    assertThatCommits(commitsAfterRename).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterRename).containsAtLeastElementsIn(commitsAfterCreation);
     assertThatCommits(commitsAfterRename).lastElement().isNotIn(commitsAfterCreation);
   }
 
@@ -584,11 +586,11 @@
 
   private static OptionalSubject<GroupReferenceSubject, GroupReference> assertThatGroup(
       Optional<GroupReference> group) {
-    return assertThat(group, GroupReferenceSubject::assertThat);
+    return assertThat(group, groupReferences());
   }
 
   private static ListSubject<CommitInfoSubject, CommitInfo> assertThatCommits(
       List<CommitInfo> commits) {
-    return ListSubject.assertThat(commits, CommitInfoSubject::assertThat);
+    return ListSubject.assertThat(commits, commits());
   }
 }
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index d3792b7..758c304 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -150,8 +150,7 @@
   private static void assertStoredRecordRoundTrip(SubmitRecord... records) {
     List<SubmitRecord> recordList = ImmutableList.copyOf(records);
     List<String> stored =
-        ChangeField.storedSubmitRecords(recordList)
-            .stream()
+        ChangeField.storedSubmitRecords(recordList).stream()
             .map(s -> new String(s, UTF_8))
             .collect(toList());
     assertThat(ChangeField.parseSubmitRecords(stored))
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 7117485..fd23da3 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -68,7 +68,7 @@
   public void nonIndexPredicate() throws Exception {
     Predicate<ChangeData> in = parse("foo:a");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(
             query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
@@ -85,7 +85,7 @@
   public void nonIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("foo:a OR foo:b");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(
             query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
@@ -96,7 +96,7 @@
   public void oneIndexPredicate() throws Exception {
     Predicate<ChangeData> in = parse("foo:a file:b");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren()).containsExactly(query(in.getChild(1)), in.getChild(0)).inOrder();
   }
 
@@ -110,7 +110,7 @@
   public void threeLevelTreeWithSomeIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(out.getClass()).isSameAs(AndChangeSource.class);
+    assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
     assertThat(out.getChildren()).containsExactly(query(in.getChild(1)), in.getChild(0)).inOrder();
   }
 
@@ -118,7 +118,7 @@
   public void multipleIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("file:a OR foo:b OR file:c OR foo:d");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(out.getClass()).isSameAs(OrSource.class);
+    assertThat(out.getClass()).isSameInstanceAs(OrSource.class);
     assertThat(out.getChildren())
         .containsExactly(query(or(in.getChild(0), in.getChild(2))), in.getChild(1), in.getChild(3))
         .inOrder();
@@ -128,7 +128,7 @@
   public void indexAndNonIndexPredicates() throws Exception {
     Predicate<ChangeData> in = parse("status:new bar:p file:a");
     Predicate<ChangeData> out = rewrite(in);
-    assertThat(AndChangeSource.class).isSameAs(out.getClass());
+    assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
     assertThat(out.getChildren())
         .containsExactly(query(and(in.getChild(0), in.getChild(2))), in.getChild(1))
         .inOrder();
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index efe6a5a..34c5717 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gwtorm.server.OrmException;
 import org.junit.Ignore;
 
 @Ignore
@@ -52,12 +51,12 @@
     }
 
     @Override
-    public ResultSet<ChangeData> read() throws OrmException {
+    public ResultSet<ChangeData> read() {
       throw new UnsupportedOperationException();
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       throw new UnsupportedOperationException("not implemented");
     }
 
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 7a5ed41..0753127 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -24,7 +24,7 @@
 public class FakeQueryBuilder extends ChangeQueryBuilder {
   FakeQueryBuilder(ChangeIndexCollection indexes) {
     super(
-        new FakeQueryBuilder.Definition<>(FakeQueryBuilder.class),
+        new ChangeQueryBuilder.Definition<>(FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
             null, null, null, null, indexes, null, null, null, null, null, null, null));
diff --git a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
index 87e98fd..0682bb3 100644
--- a/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/CommentSenderTest.java
@@ -17,13 +17,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 extends GerritBaseTests {
   private static class TestSender extends CommentSender {
-    TestSender() throws OrmException {
+    TestSender() {
       super(null, null, null, null, null);
     }
   }
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index be523d8..a1f318f 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -42,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;
@@ -57,7 +57,6 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestChanges;
 import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -147,8 +146,8 @@
                     .annotatedWith(CanonicalWebUrl.class)
                     .toInstance("http://localhost:8080/");
                 bind(Boolean.class)
-                    .annotatedWith(DisableReverseDnsLookup.class)
-                    .toInstance(Boolean.FALSE);
+                    .annotatedWith(EnableReverseDnsLookup.class)
+                    .toInstance(Boolean.TRUE);
                 bind(Realm.class).to(FakeRealm.class);
                 bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
                 bind(AccountCache.class).toInstance(accountCache);
@@ -213,7 +212,7 @@
     return update;
   }
 
-  protected ChangeNotes newNotes(Change c) throws OrmException {
+  protected ChangeNotes newNotes(Change c) {
     return new ChangeNotes(args, c, true, null).load();
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 2931b17..93c00d7 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -704,6 +704,18 @@
   }
 
   @Test
+  public void serializeUpdateCount() throws Exception {
+    assertRoundTrip(
+        newBuilder().updateCount(234).build(),
+        ChangeNotesStateProto.newBuilder()
+            .setMetaId(SHA_BYTES)
+            .setChangeId(ID.get())
+            .setColumns(colsProto)
+            .setUpdateCount(234)
+            .build());
+  }
+
+  @Test
   public void changeNotesStateMethods() throws Exception {
     assertThatSerializedClass(ChangeNotesState.class)
         .hasAutoValueMethods(
@@ -733,6 +745,7 @@
                 .put(
                     "publishedComments",
                     new TypeLiteral<ImmutableListMultimap<RevId, Comment>>() {}.getType())
+                .put("updateCount", int.class)
                 .build());
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 8892e84..a57a984 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -35,6 +35,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -54,7 +55,6 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.LinkedHashSet;
@@ -407,10 +407,7 @@
 
     ChangeNotes notes = newNotes(c);
     ImmutableList<PatchSetApproval> approvals =
-        notes
-            .getApprovals()
-            .get(c.currentPatchSetId())
-            .stream()
+        notes.getApprovals().get(c.currentPatchSetId()).stream()
             .sorted(comparing(a -> a.getAccountId().get()))
             .collect(toImmutableList());
     assertThat(approvals).hasSize(2);
@@ -996,7 +993,7 @@
     try {
       newNotes(c);
       fail("Expected IOException");
-    } catch (OrmException e) {
+    } catch (StorageException e) {
       assertCause(
           e,
           ConfigInvalidException.class,
@@ -3019,11 +3016,27 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setRevertOf(newChange().getId().get());
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("Given ChangeUpdate is only allowed on initial commit");
     update.commit();
   }
 
+  @Test
+  public void updateCount() throws Exception {
+    Change c = newChange();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(1);
+
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) -1);
+    update.commit();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(2);
+
+    update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) 1);
+    update.commit();
+    assertThat(newNotes(c).getUpdateCount()).isEqualTo(3);
+  }
+
   private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
     ObjectId dataId = notes.revisionNoteMap.noteMap.getNote(noteId).getData();
     return new String(rw.getObjectReader().open(dataId, OBJ_BLOB).getCachedBytes(), UTF_8);
diff --git a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
deleted file mode 100644
index fbec5e6..0000000
--- a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
+++ /dev/null
@@ -1,597 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.notedb.CommentJsonMigrator.ProjectMigrationResult;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.TestChanges;
-import com.google.inject.Inject;
-import java.io.ByteArrayOutputStream;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Before;
-import org.junit.Test;
-
-public class CommentJsonMigratorTest extends AbstractChangeNotesTest {
-  private CommentJsonMigrator migrator;
-  @Inject private ChangeNoteUtil noteUtil;
-  @Inject private CommentsUtil commentsUtil;
-  @Inject private LegacyChangeNoteWrite legacyChangeNoteWrite;
-  @Inject private AllUsersName allUsersName;
-
-  private AtomicInteger uuidCounter;
-
-  @Before
-  public void setUpCounter() {
-    uuidCounter = new AtomicInteger();
-    migrator = new CommentJsonMigrator(new ChangeNoteJson(), "gerrit", allUsersName);
-  }
-
-  @Test
-  public void noOpIfAllCommentsAreJson() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Comment ps1Comment = newComment(notes, 1, "comment on ps1");
-    update.putComment(Status.PUBLISHED, ps1Comment);
-    update.commit();
-
-    notes = newNotes(c);
-    update = newUpdate(c, changeOwner);
-    Comment ps2Comment = newComment(notes, 2, "comment on ps2");
-    update.putComment(Status.PUBLISHED, ps2Comment);
-    update.commit();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString());
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of());
-    assertNoDifferences(notes, oldNotes);
-    assertThat(notes.getMetaId()).isEqualTo(oldNotes.getMetaId());
-  }
-
-  @Test
-  public void migratePublishedComments() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-
-    Comment ps1Comment1 = newComment(notes, 1, "first comment on ps1");
-    Comment ps2Comment1 = newComment(notes, 2, "first comment on ps2");
-    Comment ps1Comment2 = newComment(notes, 1, "second comment on ps1");
-
-    // Construct legacy format 'by hand'.
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment1).build(), out1);
-
-    ByteArrayOutputStream out2 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(2, ps2Comment1).build(), out2);
-
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder()
-            .put(1, ps1Comment2)
-            .put(1, ps1Comment1)
-            .build(),
-        out3);
-
-    TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
-
-    String metaRefName = RefNames.changeMetaRef(c.getId());
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 1\n\nPatch-set: 1")
-        .add(ps1Comment1.revId, out1.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 2\n\nPatch-set: 2")
-        .add(ps2Comment1.revId, out2.toString())
-        .add(ps1Comment1.revId, out3.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment1.toString(),
-            getRevId(notes, 1), ps1Comment2.toString(),
-            getRevId(notes, 2), ps2Comment1.toString());
-
-    // Comments at each commit all have legacy format.
-    ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(oldLog).hasSize(4);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2)))
-        .containsExactly(ps1Comment1.key, true);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
-        .containsExactly(ps1Comment1.key, true, ps1Comment2.key, true, ps2Comment1.key, true);
-
-    // Check that dryRun doesn't touch anything.
-    String refName = RefNames.changeMetaRef(c.getId());
-    ObjectId before = repo.getRefDatabase().getRef(refName).getObjectId();
-    ProjectMigrationResult dryRunResult = migrator.migrateProject(project, repo, true);
-    ObjectId after = repo.getRefDatabase().getRef(refName).getObjectId();
-    assertThat(before).isEqualTo(after);
-    assertThat(dryRunResult.refsUpdated).isEqualTo(ImmutableList.of(refName));
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of(refName));
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertNoDifferences(notes, oldNotes);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment1.toString(),
-            getRevId(notes, 1), ps1Comment2.toString(),
-            getRevId(notes, 2), ps2Comment1.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2)))
-        .containsExactly(ps1Comment1.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
-        .containsExactly(ps1Comment1.key, false, ps1Comment2.key, false, ps2Comment1.key, false);
-  }
-
-  @Test
-  public void migrateDraftComments() throws Exception {
-    Change c = newChange();
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-    ObjectId origMetaId = notes.getMetaId();
-
-    Comment ownerCommentPs1 = newComment(notes, 1, "owner comment on ps1", changeOwner);
-    Comment ownerCommentPs2 = newComment(notes, 2, "owner comment on ps2", changeOwner);
-    Comment otherCommentPs1 = newComment(notes, 1, "other user comment on ps1", otherUser);
-
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ownerCommentPs1).build(), out1);
-
-    ByteArrayOutputStream out2 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(2, ownerCommentPs2).build(), out2);
-
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, otherCommentPs1).build(), out3);
-
-    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
-        RevWalk allUsersRw = new RevWalk(allUsersRepo)) {
-      TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, allUsersRw);
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
-          .commit()
-          .message("Review ps 1\n\nPatch-set: 1")
-          .add(ownerCommentPs1.revId, out1.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
-          .commit()
-          .message("Review ps 1\n\nPatch-set: 2")
-          .add(ownerCommentPs2.revId, out2.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-
-      testRepository
-          .branch(RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()))
-          .commit()
-          .message("Review ps 2\n\nPatch-set: 2")
-          .add(otherCommentPs1.revId, out3.toString())
-          .author(serverIdent)
-          .committer(serverIdent)
-          .create();
-    }
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
-        .containsExactly(
-            getRevId(notes, 1), ownerCommentPs1.toString(),
-            getRevId(notes, 2), ownerCommentPs2.toString());
-    assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
-        .containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
-
-    // Comments at each commit all have legacy format.
-    ImmutableList<RevCommit> oldOwnerLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
-    assertThat(oldOwnerLog).hasSize(2);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(0)))
-        .containsExactly(ownerCommentPs1.key, true);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(1)))
-        .containsExactly(ownerCommentPs1.key, true, ownerCommentPs2.key, true);
-
-    ImmutableList<RevCommit> oldOtherLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
-    assertThat(oldOtherLog).hasSize(1);
-    assertThat(getLegacyFormatMapForDraftComments(notes, oldOtherLog.get(0)))
-        .containsExactly(otherCommentPs1.key, true);
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(
-        allUsers,
-        ImmutableList.of(
-            RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()),
-            RefNames.refsDraftComments(c.getId(), otherUser.getAccountId())));
-    assertNoDifferences(notes, oldNotes);
-
-    // Migration doesn't touch change ref.
-    assertThat(repo.exactRef(RefNames.changeMetaRef(c.getId())).getObjectId())
-        .isEqualTo(origMetaId);
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
-        .containsExactly(
-            getRevId(notes, 1), ownerCommentPs1.toString(),
-            getRevId(notes, 2), ownerCommentPs2.toString());
-    assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
-        .containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newOwnerLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(0)))
-        .containsExactly(ownerCommentPs1.key, false);
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(1)))
-        .containsExactly(ownerCommentPs1.key, false, ownerCommentPs2.key, false);
-
-    ImmutableList<RevCommit> newOtherLog =
-        log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
-    assertThat(getLegacyFormatMapForDraftComments(notes, newOtherLog.get(0)))
-        .containsExactly(otherCommentPs1.key, false);
-  }
-
-  @Test
-  public void migrateMixOfJsonAndLegacyComments() throws Exception {
-    // 3 comments: legacy, JSON, legacy. Because adding a comment necessarily rewrites the entire
-    // note, these comments need to be on separate patch sets.
-    Change c = newChange();
-    incrementPatchSet(c);
-    incrementPatchSet(c);
-
-    ChangeNotes notes = newNotes(c);
-
-    Comment ps1Comment = newComment(notes, 1, "comment on ps1 (legacy)");
-
-    ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment).build(), out1);
-
-    TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
-
-    String metaRefName = RefNames.changeMetaRef(c.getId());
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 1\n\nPatch-set: 1")
-        .add(ps1Comment.revId, out1.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    Comment ps2Comment = newComment(notes, 2, "comment on ps2 (JSON)");
-    update.putComment(Status.PUBLISHED, ps2Comment);
-    update.commit();
-
-    Comment ps3Comment = newComment(notes, 3, "comment on ps3 (legacy)");
-    ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
-    legacyChangeNoteWrite.buildNote(
-        ImmutableListMultimap.<Integer, Comment>builder().put(3, ps3Comment).build(), out3);
-
-    testRepository
-        .branch(metaRefName)
-        .commit()
-        .message("Review ps 3\n\nPatch-set: 3")
-        .add(ps3Comment.revId, out3.toString())
-        .author(serverIdent)
-        .committer(serverIdent)
-        .create();
-
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString(),
-            getRevId(notes, 3), ps3Comment.toString());
-
-    // Comments at each commit match expected format.
-    ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(oldLog).hasSize(6);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
-        .containsExactly(ps1Comment.key, true);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(4)))
-        .containsExactly(ps1Comment.key, true, ps2Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(5)))
-        .containsExactly(ps1Comment.key, true, ps2Comment.key, false, ps3Comment.key, true);
-
-    ChangeNotes oldNotes = notes;
-    checkMigrate(project, ImmutableList.of(RefNames.changeMetaRef(c.getId())));
-    assertNoDifferences(notes, oldNotes);
-
-    // Comment content is the same.
-    notes = newNotes(c);
-    assertThat(getToStringRepresentations(notes.getComments()))
-        .containsExactly(
-            getRevId(notes, 1), ps1Comment.toString(),
-            getRevId(notes, 2), ps2Comment.toString(),
-            getRevId(notes, 3), ps3Comment.toString());
-
-    // Comments at each commit all have JSON format.
-    ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2))).isEmpty();
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
-        .containsExactly(ps1Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(4)))
-        .containsExactly(ps1Comment.key, false, ps2Comment.key, false);
-    assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(5)))
-        .containsExactly(ps1Comment.key, false, ps2Comment.key, false, ps3Comment.key, false);
-  }
-
-  private void checkMigrate(Project.NameKey project, List<String> expectedRefs) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
-
-      assertThat(progress.ok).isTrue();
-      assertThat(progress.refsUpdated).isEqualTo(expectedRefs);
-    }
-  }
-
-  private Comment newComment(ChangeNotes notes, int psNum, String message) {
-    return newComment(notes, psNum, message, changeOwner);
-  }
-
-  private Comment newComment(
-      ChangeNotes notes, int psNum, String message, IdentifiedUser commenter) {
-    return newComment(
-        new PatchSet.Id(notes.getChangeId(), psNum),
-        "filename",
-        "uuid-" + uuidCounter.getAndIncrement(),
-        null,
-        0,
-        commenter,
-        null,
-        TimeUtil.nowTs(),
-        message,
-        (short) 1,
-        getRevId(notes, psNum).get(),
-        false);
-  }
-
-  private void incrementPatchSet(Change c) throws Exception {
-    TestChanges.incrementPatchSet(c);
-    RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setCommit(rw, commit);
-    update.commit();
-  }
-
-  private static RevId getRevId(ChangeNotes notes, int psNum) {
-    PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), psNum);
-    PatchSet ps = notes.getPatchSets().get(psId);
-    checkArgument(ps != null, "no patch set %s: %s", psNum, notes.getPatchSets());
-    return ps.getRevision();
-  }
-
-  private static ListMultimap<RevId, String> getToStringRepresentations(
-      ListMultimap<RevId, Comment> comments) {
-    // Use string representation for equality comparison in this test, because Comment#equals only
-    // compares keys.
-    return Multimaps.transformValues(comments, Comment::toString);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForPublishedComments(
-      ChangeNotes notes, ObjectId metaId) throws Exception {
-    return getLegacyFormatMap(project, notes.getChangeId(), metaId, Status.PUBLISHED);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForDraftComments(
-      ChangeNotes notes, ObjectId metaId) throws Exception {
-    return getLegacyFormatMap(allUsers, notes.getChangeId(), metaId, Status.DRAFT);
-  }
-
-  private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMap(
-      Project.NameKey project, Change.Id changeId, ObjectId metaId, Status status)
-      throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        ObjectReader reader = repo.newObjectReader();
-        RevWalk rw = new RevWalk(reader)) {
-      NoteMap noteMap = NoteMap.read(reader, rw.parseCommit(metaId));
-      RevisionNoteMap<ChangeRevisionNote> revNoteMap =
-          RevisionNoteMap.parse(
-              noteUtil.getChangeNoteJson(),
-              noteUtil.getLegacyChangeNoteRead(),
-              changeId,
-              reader,
-              noteMap,
-              status);
-      return revNoteMap
-          .revisionNotes
-          .values()
-          .stream()
-          .flatMap(crn -> crn.getComments().stream())
-          .collect(toImmutableMap(c -> c.key, c -> c.legacyFormat));
-    }
-  }
-
-  private ImmutableList<RevCommit> log(Project.NameKey project, String refName) throws Exception {
-    try (Repository repo = repoManager.openRepository(project)) {
-      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);
-      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());
-    for (int i = 0; i < expectedLog.size(); i++) {
-      RevCommit actual = actualLog.get(i);
-      RevCommit expected = expectedLog.get(i);
-      assertThat(actual.getAuthorIdent())
-          .named("author of entry %s", i)
-          .isEqualTo(expected.getAuthorIdent());
-      assertThat(actual.getCommitterIdent())
-          .named("committer of entry %s", i)
-          .isEqualTo(expected.getCommitterIdent());
-      assertThat(actual.getFullMessage()).named("message of entry %s", i).isNotNull();
-      assertThat(actual.getFullMessage())
-          .named("message of entry %s", i)
-          .isEqualTo(expected.getFullMessage());
-    }
-  }
-
-  private void assertNoDifferences(ChangeNotes actual, ChangeNotes expected) throws Exception {
-    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/IntBlobTest.java b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
index 1abaa22..1cbe61d 100644
--- a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
+++ b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
@@ -18,10 +18,10 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -87,8 +87,8 @@
     ObjectId id = tr.update(refName, tr.blob("1 2 3"));
     try {
       IntBlob.parse(repo, refName);
-      assert_().fail("Expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("Expected StorageException");
+    } catch (StorageException e) {
       assertThat(e).hasMessageThat().isEqualTo("invalid value in refs/foo blob at " + id.name());
     }
   }
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index 5aad6ff..263873d 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -23,12 +23,12 @@
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
 import com.google.common.util.concurrent.Runnables;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -67,7 +67,7 @@
       for (int i = 1; i <= max; i++) {
         try {
           assertThat(s.next()).named("i=" + i + " for " + name).isEqualTo(i);
-        } catch (OrmException e) {
+        } catch (StorageException e) {
           throw new AssertionError("failed batchSize=" + batchSize + ", i=" + i, e);
         }
       }
@@ -168,7 +168,7 @@
   @Test
   public void failOnInvalidValue() throws Exception {
     ObjectId id = writeBlob("id", "not a number");
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("invalid value in refs/sequences/id blob at " + id.name());
     newSequence("id", 1, 3).next();
   }
@@ -181,7 +181,7 @@
       try {
         newSequence("id", 1, 3).next();
         fail();
-      } catch (OrmException e) {
+      } catch (StorageException e) {
         assertThat(e.getCause()).isInstanceOf(ExecutionException.class);
         assertThat(e.getCause().getCause()).isInstanceOf(IncorrectObjectTypeException.class);
       }
@@ -200,7 +200,7 @@
             RetryerBuilder.<RefUpdate>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.next();
   }
@@ -335,7 +335,7 @@
             RetryerBuilder.<RefUpdate>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
-    exception.expect(OrmException.class);
+    exception.expect(StorageException.class);
     exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.increaseTo(2);
   }
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 872cd91..5f08875 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -41,7 +41,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -54,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;
@@ -931,7 +930,6 @@
   }
 
   private InMemoryRepository add(ProjectConfig pc) {
-    SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
 
     InMemoryRepository repo;
@@ -946,7 +944,6 @@
     all.put(
         pc.getName(),
         new ProjectState(
-            sitePaths,
             projectCache,
             allProjectsName,
             allUsersName,
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 79c620a..cf6d50f 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -222,9 +222,7 @@
             .getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
             .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
 
-    return adminPermission
-        .getRules()
-        .stream()
+    return adminPermission.getRules().stream()
         .map(PermissionRule::getGroup)
         .map(GroupReference::getUUID)
         .collect(ImmutableList.toImmutableList());
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 58ea1d2..4bdb763 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,7 +50,6 @@
 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.StarsInput;
@@ -64,24 +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.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.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;
@@ -92,6 +93,7 @@
 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;
@@ -99,6 +101,7 @@
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
+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;
@@ -156,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;
@@ -524,8 +528,7 @@
 
     // Convert AccountInfos to strings, either account ID or email.
     List<String> reviewerIds =
-        reviewers
-            .stream()
+        reviewers.stream()
             .map(
                 ai -> {
                   if (ai._accountId != null) {
@@ -1323,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);
@@ -1343,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.
@@ -1360,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");
@@ -1380,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);
@@ -1614,6 +1823,8 @@
     Change[] expected = new Change[] {change6, change5, change4, change3, change2, change1};
     assertQuery("user@example.com", expected);
     assertQuery("repo", expected);
+
+    assertQuery("Code-Review:+1", change4);
   }
 
   @Test
@@ -1652,6 +1863,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));
@@ -2007,10 +2236,7 @@
     gApi.groups().id(group).addMembers(user2.toString(), user3.toString());
 
     List<String> members =
-        gApi.groups()
-            .id(group)
-            .members()
-            .stream()
+        gApi.groups().id(group).members().stream()
             .map(a -> a._accountId.toString())
             .collect(toList());
     assertThat(members).contains(user2.toString());
@@ -2433,7 +2659,7 @@
       return this;
     }
 
-    DashboardChangeState draftAndDeleteCommentBy(Id commenterId) {
+    DashboardChangeState draftAndDeleteCommentBy(Account.Id commenterId) {
       deleteDraftCommentBy.add(commenterId);
       return this;
     }
@@ -2870,6 +3096,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);
   }
@@ -2879,6 +3163,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);
@@ -2962,7 +3255,6 @@
     PatchSetInserter inserter =
         patchSetFactory
             .create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit)
-            .setNotify(NotifyHandling.NONE)
             .setFireRevisionCreated(false)
             .setValidate(false);
     try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
@@ -2970,6 +3262,7 @@
         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();
     }
@@ -2990,6 +3283,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)));
@@ -3025,21 +3327,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 {
@@ -3121,12 +3434,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 1ccac32..e8a63b8 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -21,7 +21,6 @@
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
@@ -43,7 +42,6 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
@@ -60,6 +58,7 @@
     ),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/proto/testing",
         "//java/com/google/gerrit/reviewdb:server",
@@ -67,7 +66,6 @@
         "//java/com/google/gerrit/server/cache/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index f7252c0..aba0018 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -37,6 +37,6 @@
     PatchSet ps2 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 2));
     cd.setPatchSets(ImmutableList.of(ps1, ps2));
     PatchSet curr2 = cd.currentPatchSet();
-    assertThat(curr2).isNotSameAs(curr1);
+    assertThat(curr2).isNotSameInstanceAs(curr1);
   }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index 9944a42..135e9c26 100644
--- a/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/javatests/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -20,13 +20,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 extends GerritBaseTests {
   @Test
-  public void prefixOnlyOptimization() throws OrmException {
+  public void prefixOnlyOptimization() {
     RegexPathPredicate p = predicate("^a/b/.*");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("source.c")));
@@ -36,7 +35,7 @@
   }
 
   @Test
-  public void prefixReducesSearchSpace() throws OrmException {
+  public void prefixReducesSearchSpace() {
     RegexPathPredicate p = predicate("^a/b/.*\\.[ch]");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("a/b/source.res")));
@@ -46,7 +45,7 @@
   }
 
   @Test
-  public void fileExtension_Constant() throws OrmException {
+  public void fileExtension_Constant() {
     RegexPathPredicate p = predicate("^.*\\.res");
     assertTrue(p.match(change("test.res")));
     assertTrue(p.match(change("foo/bar/test.res")));
@@ -54,7 +53,7 @@
   }
 
   @Test
-  public void fileExtension_CharacterGroup() throws OrmException {
+  public void fileExtension_CharacterGroup() {
     RegexPathPredicate p = predicate("^.*\\.[ch]");
     assertTrue(p.match(change("test.c")));
     assertTrue(p.match(change("test.h")));
@@ -62,7 +61,7 @@
   }
 
   @Test
-  public void endOfString() throws OrmException {
+  public void endOfString() {
     assertTrue(predicate("^a$").match(change("a")));
     assertFalse(predicate("^a$").match(change("a$")));
 
@@ -71,7 +70,7 @@
   }
 
   @Test
-  public void exactMatch() throws OrmException {
+  public void exactMatch() {
     RegexPathPredicate p = predicate("^foo.c");
     assertTrue(p.match(change("foo.c")));
     assertFalse(p.match(change("foo.cc")));
@@ -82,7 +81,7 @@
     return new RegexPathPredicate(pattern);
   }
 
-  private static ChangeData change(String... files) throws OrmException {
+  private static ChangeData change(String... files) {
     Arrays.sort(files);
     ChangeData cd = ChangeData.createForTest(new Project.NameKey("project"), new Change.Id(1), 1);
     cd.setCurrentFilePaths(Arrays.asList(files));
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 5c828ba..08ef2b0 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
@@ -48,6 +49,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -62,6 +64,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -93,6 +96,8 @@
 
   @Inject protected AllProjectsName allProjects;
 
+  @Inject protected AllUsersName allUsers;
+
   protected LifecycleManager lifecycle;
   protected Injector injector;
   protected AccountInfo currentUserInfo;
@@ -160,6 +165,25 @@
   }
 
   @Test
+  public void byParent() throws Exception {
+    assertQuery("parent:project");
+    ProjectInfo parent = createProject(name("parent"));
+    assertQuery("parent:" + parent.name);
+    ProjectInfo child = createProject(name("child"), parent.name);
+    assertQuery("parent:" + parent.name, child);
+  }
+
+  @Test
+  public void byParentOfAllProjects() throws Exception {
+    Set<String> excludedProjects = ImmutableSet.of(allProjects.get(), allUsers.get());
+    ProjectInfo[] projects =
+        gApi.projects().list().get().stream()
+            .filter(p -> !excludedProjects.contains(p.name))
+            .toArray(s -> new ProjectInfo[s]);
+    assertQuery("parent:" + allProjects.get(), projects);
+  }
+
+  @Test
   public void byInname() throws Exception {
     String namePart = getSanitizedMethodName();
     namePart = CharMatcher.is('_').removeFrom(namePart);
@@ -296,6 +320,13 @@
     return gApi.projects().create(in).get();
   }
 
+  protected ProjectInfo createProject(String name, String parent) throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name;
+    in.parent = parent;
+    return gApi.projects().create(in).get();
+  }
+
   protected ProjectInfo createProjectWithDescription(String name, String description)
       throws Exception {
     ProjectInput in = new ProjectInput();
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/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/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index 0d45d43..b6887cf 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -22,18 +22,18 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.Sequences;
 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;
@@ -64,8 +64,8 @@
   public void downgradeNotSupported() throws Exception {
     try {
       requiredUpgrades(14, versions(10, 11, 12, 13));
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e)
           .hasMessageThat()
           .contains("Cannot downgrade NoteDb schema from version 14 to 13");
@@ -78,8 +78,8 @@
     assertThat(requiredUpgrades(9, versions)).containsExactly(10, 11, 12).inOrder();
     try {
       requiredUpgrades(8, versions);
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10");
     }
   }
@@ -99,7 +99,7 @@
       allUsersName = new AllUsersName("The-Users");
       repoManager = new InMemoryRepositoryManager();
 
-      args = new NoteDbSchemaVersion.Arguments(repoManager, allProjectsName);
+      args = new NoteDbSchemaVersion.Arguments(repoManager, allProjectsName, allUsersName);
       NoteDbSchemaVersionManager versionManager =
           new NoteDbSchemaVersionManager(allProjectsName, repoManager);
       updater =
@@ -122,21 +122,21 @@
       }
 
       @Override
-      public void create() throws OrmException, IOException {
+      public void create() throws 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);
+          throw new StorageException(e);
         }
         repoManager.createRepository(allUsersName).close();
         setUp();
       }
 
       @Override
-      public void ensureCreated() throws OrmException, IOException {
+      public void ensureCreated() throws IOException {
         try {
           repoManager.openRepository(allProjectsName).close();
         } catch (RepositoryNotFoundException e) {
@@ -152,7 +152,7 @@
       cfg.setString("noteDb", "changes", "disableReviewDb", "true");
     }
 
-    protected void seedGroupSequenceRef() throws OrmException {
+    protected void seedGroupSequenceRef() {
       new RepoSequence(
               repoManager,
               GitReferenceUpdated.DISABLED,
@@ -163,12 +163,8 @@
           .next();
     }
 
-    /**
-     * Test-specific setup.
-     *
-     * @throws OrmException if an error occurs.
-     */
-    protected void setUp() throws OrmException {}
+    /** Test-specific setup. */
+    protected void setUp() {}
 
     ImmutableList<String> update() throws Exception {
       updater.update(
@@ -191,26 +187,16 @@
       }
     }
 
-    private static class TestSchema_10 implements NoteDbSchemaVersion {
-      @SuppressWarnings("unused")
-      TestSchema_10(Arguments args) {
-        // Do nothing.
-      }
-
+    static class TestSchema_10 implements NoteDbSchemaVersion {
       @Override
-      public void upgrade(UpdateUI ui) {
+      public void upgrade(Arguments args, UpdateUI ui) {
         ui.message("body of 10");
       }
     }
 
-    private static class TestSchema_11 implements NoteDbSchemaVersion {
-      @SuppressWarnings("unused")
-      TestSchema_11(Arguments args) {
-        // Do nothing.
-      }
-
+    static class TestSchema_11 implements NoteDbSchemaVersion {
       @Override
-      public void upgrade(UpdateUI ui) {
+      public void upgrade(Arguments args, UpdateUI ui) {
         ui.message("BODY OF 11");
       }
     }
@@ -221,7 +207,7 @@
     TestUpdate u =
         new TestUpdate(Optional.empty()) {
           @Override
-          public void setUp() throws OrmException {
+          public void setUp() {
             setNotesMigrationConfig();
             seedGroupSequenceRef();
           }
@@ -241,14 +227,14 @@
     TestUpdate u =
         new TestUpdate(Optional.empty()) {
           @Override
-          public void setUp() throws OrmException {
+          public void setUp() {
             seedGroupSequenceRef();
           }
         };
     try {
       u.update();
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e).hasMessageThat().contains("NoteDb change migration was not completed");
     }
     assertThat(u.getMessages()).isEmpty();
@@ -266,8 +252,8 @@
         };
     try {
       u.update();
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e).hasMessageThat().contains("upgrade to 2.16.x first");
     }
     assertThat(u.getMessages()).isEmpty();
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
index 5ea2a7a..9c62d7f 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
@@ -1,14 +1,28 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package com.google.gerrit.server.schema;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
 
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.server.OrmException;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
@@ -43,8 +57,8 @@
     tr.update(REFS_VERSION, blobId);
     try {
       manager.read();
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e)
           .hasMessageThat()
           .isEqualTo("invalid value in refs/meta/version blob at " + blobId.name());
@@ -69,8 +83,8 @@
     tr.update(REFS_VERSION, tr.blob("123"));
     try {
       manager.increment(456);
-      assert_().fail("expected OrmException");
-    } catch (OrmException e) {
+      assert_().fail("expected StorageException");
+    } catch (StorageException e) {
       assertThat(e)
           .hasMessageThat()
           .isEqualTo("Expected old version 456 for refs/meta/version, found 123");
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
index 042ac30..7bc3848 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
@@ -53,8 +53,7 @@
     int minNoteDbVersion = 180;
     ImmutableList<Integer> allSchemaVersions =
         ClassPath.from(getClass().getClassLoader())
-            .getTopLevelClasses(getClass().getPackage().getName())
-            .stream()
+            .getTopLevelClasses(getClass().getPackage().getName()).stream()
             .map(ClassInfo::load)
             .map(NoteDbSchemaVersions::guessVersion)
             .flatMap(Streams::stream)
@@ -68,9 +67,8 @@
 
   @Test
   public void schemaConstructors() throws Exception {
-    NoteDbSchemaVersion.Arguments args = new NoteDbSchemaVersion.Arguments(null, null);
     for (int version : NoteDbSchemaVersions.ALL.keySet()) {
-      NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version, args);
+      NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version);
     }
   }
 }
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index 9117e85..6831fa3 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -4,16 +4,19 @@
     name = "small_tests",
     size = "small",
     srcs = glob(["*.java"]),
+    runtime_deps = ["//prolog:gerrit-prolog-common"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 2ae0a5f..fe30901 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -14,17 +14,36 @@
 
 package com.google.gerrit.server.update;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.TooManyUpdatesException;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
@@ -32,11 +51,24 @@
 import org.junit.Test;
 
 public class BatchUpdateTest extends GerritBaseTests {
-  @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
+  private static final int MAX_UPDATES = 4;
 
-  @Inject private GitRepositoryManager repoManager;
+  @Rule
+  public InMemoryTestEnvironment testEnvironment =
+      new InMemoryTestEnvironment(
+          () -> {
+            Config cfg = new Config();
+            cfg.setInt("change", null, "maxUpdates", MAX_UPDATES);
+            return cfg;
+          });
+
   @Inject private BatchUpdate.Factory batchUpdateFactory;
+  @Inject private ChangeInserter.Factory changeInserterFactory;
+  @Inject private ChangeNotes.Factory changeNotesFactory;
+  @Inject private GitRepositoryManager repoManager;
+  @Inject private PatchSetInserter.Factory patchSetInserterFactory;
   @Inject private Provider<CurrentUser> user;
+  @Inject private Sequences sequences;
 
   private Project.NameKey project;
   private TestRepository<Repository> repo;
@@ -51,8 +83,8 @@
 
   @Test
   public void addRefUpdateFromFastForwardCommit() throws Exception {
-    final RevCommit masterCommit = repo.branch("master").commit().create();
-    final RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
+    RevCommit masterCommit = repo.branch("master").commit().create();
+    RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
 
     try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
       bu.addRepoOnlyOp(
@@ -65,7 +97,228 @@
       bu.execute();
     }
 
-    assertEquals(
-        repo.getRepository().exactRef("refs/heads/master").getObjectId(), branchCommit.getId());
+    assertThat(repo.getRepository().exactRef("refs/heads/master").getObjectId())
+        .isEqualTo(branchCommit.getId());
+  }
+
+  @Test
+  public void cannotExceedMaxUpdates() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Excessive update"));
+      try {
+        bu.execute();
+        assert_().fail("expected ResourceConflictException");
+      } catch (ResourceConflictException e) {
+        assertThat(e).hasMessageThat().isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
+      }
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void cannotExceedMaxUpdatesCountingMultipleChangeUpdatesInSingleBatch() throws Exception {
+    Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1);
+
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Update on PS1", new PatchSet.Id(id, 1)));
+      bu.addOp(id, new AddMessageOp("Update on PS2", new PatchSet.Id(id, 2)));
+      try {
+        bu.execute();
+        assert_().fail("expected ResourceConflictException");
+      } catch (ResourceConflictException e) {
+        assertThat(e).hasMessageThat().isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
+      }
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES - 1);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithCompleteNoOp() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              return false;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithNoOpAfterPopulatingUpdate() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              ctx.getUpdate(ctx.getChange().currentPatchSetId()).setChangeMessage("No-op");
+              return false;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
+    assertThat(getMetaId(id)).isEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithSubmit() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new SubmitOp());
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithSubmitAfterOtherOp() throws Exception {
+    Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(id, new AddMessageOp("Message on PS1", new PatchSet.Id(id, 1)));
+      bu.addOp(id, new SubmitOp());
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+  // Not possible to write a variant of this test that submits first and adds a message second in
+  // the same batch, since submit always comes last.
+
+  @Test
+  public void exceedingMaxUpdatesAllowedWithAbandon() throws Exception {
+    Change.Id id = createChangeWithUpdates(MAX_UPDATES);
+    ObjectId oldMetaId = getMetaId(id);
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdateOp() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+              update.setChangeMessage("Abandon");
+              update.setStatus(Change.Status.ABANDONED);
+              return true;
+            }
+          });
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES + 1);
+    assertThat(getMetaId(id)).isNotEqualTo(oldMetaId);
+  }
+
+  private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception {
+    checkArgument(totalUpdates > 0);
+    checkArgument(totalUpdates <= MAX_UPDATES);
+    Change.Id id = new Change.Id(sequences.nextChangeId());
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      bu.insertChange(
+          changeInserterFactory.create(
+              id, repo.commit().message("Change").insertChangeId().create(), "refs/heads/master"));
+      bu.execute();
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(1);
+    for (int i = 2; i <= totalUpdates; i++) {
+      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+        bu.addOp(id, new AddMessageOp("Update " + i));
+        bu.execute();
+      }
+    }
+    assertThat(getUpdateCount(id)).isEqualTo(totalUpdates);
+    return id;
+  }
+
+  private Change.Id createChangeWithTwoPatchSets(int totalUpdates) throws Exception {
+    Change.Id id = createChangeWithUpdates(totalUpdates - 1);
+    ChangeNotes notes = changeNotesFactory.create(project, id);
+
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      ObjectId commitId =
+          repo.amend(ObjectId.fromString(notes.getCurrentPatchSet().getRevision().get()))
+              .message("PS2")
+              .create();
+      bu.addOp(
+          id,
+          patchSetInserterFactory
+              .create(notes, new PatchSet.Id(id, 2), commitId)
+              .setMessage("Add PS2"));
+      bu.execute();
+    }
+
+    assertThat(getUpdateCount(id)).isEqualTo(totalUpdates);
+    return id;
+  }
+
+  private static class AddMessageOp implements BatchUpdateOp {
+    private final String message;
+    @Nullable private final PatchSet.Id psId;
+
+    AddMessageOp(String message) {
+      this(message, null);
+    }
+
+    AddMessageOp(String message, PatchSet.Id psId) {
+      this.message = message;
+      this.psId = psId;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      PatchSet.Id psIdToUpdate = psId;
+      if (psIdToUpdate == null) {
+        psIdToUpdate = ctx.getChange().currentPatchSetId();
+      } else {
+        checkState(
+            ctx.getNotes().getPatchSets().containsKey(psIdToUpdate),
+            "%s not in %s",
+            psIdToUpdate,
+            ctx.getNotes().getPatchSets().keySet());
+      }
+      ctx.getUpdate(psIdToUpdate).setChangeMessage(message);
+      return true;
+    }
+  }
+
+  private int getUpdateCount(Change.Id changeId) throws Exception {
+    return changeNotesFactory.create(project, changeId).getUpdateCount();
+  }
+
+  private ObjectId getMetaId(Change.Id changeId) throws Exception {
+    return repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId();
+  }
+
+  private static class SubmitOp implements BatchUpdateOp {
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      SubmitRecord sr = new SubmitRecord();
+      sr.status = SubmitRecord.Status.OK;
+      SubmitRecord.Label cr = new SubmitRecord.Label();
+      cr.status = SubmitRecord.Label.Status.OK;
+      cr.appliedBy = ctx.getAccountId();
+      cr.label = "Code-Review";
+      sr.labels = ImmutableList.of(cr);
+      ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+      update.merge(new RequestId(), ImmutableList.of(sr));
+      update.setChangeMessage("Submitted");
+      return true;
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/server/util/git/BUILD b/javatests/com/google/gerrit/server/util/git/BUILD
index 096afa6..cdc823e 100644
--- a/javatests/com/google/gerrit/server/util/git/BUILD
+++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -16,7 +16,6 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:guava-retrying",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/lib/BUILD b/lib/BUILD
index 8221e13..9dce60a 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -34,20 +34,6 @@
 )
 
 java_library(
-    name = "gwtorm-client",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtorm-client//jar"],
-)
-
-java_library(
-    name = "gwtorm-client_src",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@gwtorm-client//jar:src"],
-)
-
-java_library(
     name = "protobuf",
     data = ["//lib:LICENSE-protobuf"],
     visibility = ["//visibility:public"],
@@ -55,17 +41,6 @@
 )
 
 java_library(
-    name = "gwtorm",
-    visibility = ["//visibility:public"],
-    exports = [":gwtorm-client"],
-    runtime_deps = [
-        ":protobuf",
-        "//lib/antlr:java-runtime",
-        "//lib/ow2:ow2-asm",
-    ],
-)
-
-java_library(
     name = "guava-failureaccess",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
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-resemblejs b/lib/LICENSE-resemblejs
new file mode 100644
index 0000000..b265c8a
--- /dev/null
+++ b/lib/LICENSE-resemblejs
@@ -0,0 +1,18 @@
+The MIT License (MIT) Copyright © 2013 Huddle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/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/fonts/BUILD b/lib/fonts/BUILD
index 025b93e..c4719d5 100644
--- a/lib/fonts/BUILD
+++ b/lib/fonts/BUILD
@@ -1,7 +1,6 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
-
 # Roboto Mono. Version 2.136
 # https://github.com/google/roboto/releases/tag/v2.136
+
 filegroup(
     name = "robotofonts",
     srcs = [
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/guava.bzl b/lib/guava.bzl
index e4c9083..c36bf14 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "27.0.1-jre"
+GUAVA_VERSION = "27.1-jre"
 
-GUAVA_BIN_SHA1 = "bd41a290787b5301e63929676d792c507bbc00ae"
+GUAVA_BIN_SHA1 = "e47b59c893079b87743cdcfb6f17ca95c08c592c"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js
index b3f68d8..afe3f54 100644
--- a/lib/highlightjs/highlight.min.js
+++ b/lib/highlightjs/highlight.min.js
@@ -1,29 +1,31 @@
 /*
- highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,k,q){b!=Array.prototype&&b!=Object.prototype&&(b[k]=q.value)};$jscomp.getGlobal=function(b){return"undefined"!=typeof window&&window===b?b:"undefined"!=typeof global&&null!=global?global:b};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_";
-$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.Symbol=function(){var b=0;return function(k){return $jscomp.SYMBOL_PREFIX+(k||"")+b++}}();
-$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.iterator;b||(b=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[b]&&$jscomp.defineProperty(Array.prototype,b,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(b){var k=0;return $jscomp.iteratorPrototype(function(){return k<b.length?{done:!1,value:b[k++]}:{done:!0}})};
-$jscomp.iteratorPrototype=function(b){$jscomp.initSymbolIterator();b={next:b};b[$jscomp.global.Symbol.iterator]=function(){return this};return b};$jscomp.iteratorFromArray=function(b,k){$jscomp.initSymbolIterator();b instanceof String&&(b+="");var q=0,n={next:function(){if(q<b.length){var r=q++;return{value:k(r,b[r]),done:!1}}n.next=function(){return{done:!0,value:void 0}};return n.next()}};n[Symbol.iterator]=function(){return n};return n};
-$jscomp.polyfill=function(b,k,q,n){if(k){q=$jscomp.global;b=b.split(".");for(n=0;n<b.length-1;n++){var r=b[n];r in q||(q[r]={});q=q[r]}b=b[b.length-1];n=q[b];k=k(n);k!=n&&null!=k&&$jscomp.defineProperty(q,b,{configurable:!0,writable:!0,value:k})}};$jscomp.polyfill("Array.prototype.keys",function(b){return b?b:function(){return $jscomp.iteratorFromArray(this,function(b){return b})}},"es6","es3");
-(function(b){var k="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):k&&(k.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return k.hljs}))})(function(b){function k(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function q(a,c){return(a=a&&a.exec(c))&&0===a.index}function n(a){var c,b={},e=Array.prototype.slice.call(arguments,1);for(c in a)b[c]=a[c];e.forEach(function(a){for(c in a)b[c]=a[c]});
-return b}function r(a){var c=[];(function g(a,b){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?b+=a.nodeValue.length:1===a.nodeType&&(c.push({event:"start",offset:b,node:a}),b=g(a,b),a.nodeName.toLowerCase().match(/br|hr|img|input/)||c.push({event:"stop",offset:b,node:a}));return b})(a,0);return c}function K(a,c,b){function d(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function f(a){m+="<"+a.nodeName.toLowerCase()+G.map.call(a.attributes,
-function(a){return" "+a.nodeName+'="'+k(a.value).replace('"',"&quot;")+'"'}).join("")+">"}function g(a){m+="</"+a.nodeName.toLowerCase()+">"}function l(a){("start"===a.event?f:g)(a.node)}for(var p=0,m="",u=[];a.length||c.length;){var h=d();m+=k(b.substring(p,h[0].offset));p=h[0].offset;if(h===a){u.reverse().forEach(g);do l(h.splice(0,1)[0]),h=d();while(h===a&&h.length&&h[0].offset===p);u.reverse().forEach(f)}else"start"===h[0].event?u.push(h[0].node):u.pop(),l(h.splice(0,1)[0])}return m+k(b.substr(p))}
-function L(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(c){return n(a,{variants:null},c)}));return a.cached_variants||a.endsWithParent&&[n(a)]||[a]}function M(a){function c(a){return a&&a.source||a}function b(b,d){return new RegExp(c(b),"m"+(a.case_insensitive?"i":"")+(d?"g":""))}function e(d,g){if(!d.compiled){d.compiled=!0;d.keywords=d.keywords||d.beginKeywords;if(d.keywords){var f={},p=function(c,d){a.case_insensitive&&(d=d.toLowerCase());d.split(" ").forEach(function(a){a=
-a.split("|");f[a[0]]=[c,a[1]?Number(a[1]):1]})};"string"===typeof d.keywords?p("keyword",d.keywords):y(d.keywords).forEach(function(a){p(a,d.keywords[a])});d.keywords=f}d.lexemesRe=b(d.lexemes||/\w+/,!0);g&&(d.beginKeywords&&(d.begin="\\b("+d.beginKeywords.split(" ").join("|")+")\\b"),d.begin||(d.begin=/\B|\b/),d.beginRe=b(d.begin),d.end||d.endsWithParent||(d.end=/\B|\b/),d.end&&(d.endRe=b(d.end)),d.terminator_end=c(d.end)||"",d.endsWithParent&&g.terminator_end&&(d.terminator_end+=(d.end?"|":"")+
-g.terminator_end));d.illegal&&(d.illegalRe=b(d.illegal));null==d.relevance&&(d.relevance=1);d.contains||(d.contains=[]);d.contains=Array.prototype.concat.apply([],d.contains.map(function(a){return L("self"===a?d:a)}));d.contains.forEach(function(a){e(a,d)});d.starts&&e(d.starts,g);g=d.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([d.terminator_end,d.illegal]).map(c).filter(Boolean);d.terminators=g.length?b(g.join("|"),!0):{exec:function(){return null}}}}
-e(a)}function B(a,c,d,b){function e(a,c){if(q(a.endRe,c)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return e(a.parent,c)}function g(a,c,d,b){return'<span class="'+(b?"":w.classPrefix)+(a+'">')+c+(d?"":"</span>")}function l(){var a=x,c;if(null!=h.subLanguage)if((c="string"===typeof h.subLanguage)&&!z[h.subLanguage])c=k(t);else{var d=c?B(h.subLanguage,t,!0,n[h.subLanguage]):E(t,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(r+=d.relevance);c&&(n[h.subLanguage]=
-d.top);c=g(d.language,d.value,!1,!0)}else if(h.keywords){d="";var b=0;h.lexemesRe.lastIndex=0;for(c=h.lexemesRe.exec(t);c;){d+=k(t.substring(b,c.index));b=h;var e=c;e=u.case_insensitive?e[0].toLowerCase():e[0];(b=b.keywords.hasOwnProperty(e)&&b.keywords[e])?(r+=b[1],d+=g(b[0],k(c[0]))):d+=k(c[0]);b=h.lexemesRe.lastIndex;c=h.lexemesRe.exec(t)}c=d+k(t.substr(b))}else c=k(t);x=a+c;t=""}function p(a){x+=a.className?g(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function m(a,c){t+=a;if(null==
-c)return l(),0;a:{a=h;var b;var f=0;for(b=a.contains.length;f<b;f++)if(q(a.contains[f].beginRe,c)){a=a.contains[f];break a}a=void 0}if(a)return a.skip?t+=c:(a.excludeBegin&&(t+=c),l(),a.returnBegin||a.excludeBegin||(t=c)),p(a,c),a.returnBegin?0:c.length;if(a=e(h,c)){f=h;f.skip?t+=c:(f.returnEnd||f.excludeEnd||(t+=c),l(),f.excludeEnd&&(t=c));do h.className&&(x+="</span>"),h.skip||h.subLanguage||(r+=h.relevance),h=h.parent;while(h!==a.parent);a.starts&&p(a.starts,"");return f.returnEnd?0:c.length}if(!d&&
-q(h.illegalRe,c))throw Error('Illegal lexeme "'+c+'" for mode "'+(h.className||"<unnamed>")+'"');t+=c;return c.length||1}var u=A(a);if(!u)throw Error('Unknown language: "'+a+'"');M(u);var h=b||u,n={},x="";for(b=h;b!==u;b=b.parent)b.className&&(x=g(b.className,"",!0)+x);var t="",r=0;try{for(var v,y,C=0;;){h.terminators.lastIndex=C;v=h.terminators.exec(c);if(!v)break;y=m(c.substring(C,v.index),v[0]);C=v.index+y}m(c.substr(C));for(b=h;b.parent;b=b.parent)b.className&&(x+="</span>");return{relevance:r,
-value:x,language:a,top:h}}catch(D){if(D.message&&-1!==D.message.indexOf("Illegal"))return{relevance:0,value:k(c)};throw D;}}function E(a,c){c=c||w.languages||y(z);var d={relevance:0,value:k(a)},b=d;c.filter(A).forEach(function(c){var e=B(c,a,!1);e.language=c;e.relevance>b.relevance&&(b=e);e.relevance>d.relevance&&(b=d,d=e)});b.language&&(d.second_best=b);return d}function H(a){return w.tabReplace||w.useBR?a.replace(N,function(a,b){return w.useBR&&"\n"===a?"<br>":w.tabReplace?b.replace(/\t/g,w.tabReplace):
-""}):a}function I(a){var c,b;a:{var e=a.className+" ";e+=a.parentNode?a.parentNode.className:"";if(b=O.exec(e))b=A(b[1])?b[1]:"no-highlight";else{e=e.split(/\s+/);b=0;for(c=e.length;b<c;b++){var f=e[b];if(J.test(f)||A(f)){b=f;break a}}b=void 0}}if(!J.test(b)){w.useBR?(f=document.createElementNS("http://www.w3.org/1999/xhtml","div"),f.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):f=a;c=f.textContent;e=b?B(b,c,!0):E(c);f=r(f);if(f.length){var g=document.createElementNS("http://www.w3.org/1999/xhtml",
-"div");g.innerHTML=e.value;e.value=K(f,r(g),c)}e.value=H(e.value);a.innerHTML=e.value;c=a.className;b=b?F[b]:e.language;f=[c.trim()];c.match(/\bhljs\b/)||f.push("hljs");-1===c.indexOf(b)&&f.push(b);b=f.join(" ").trim();a.className=b;a.result={language:e.language,re:e.relevance};e.second_best&&(a.second_best={language:e.second_best.language,re:e.second_best.relevance})}}function v(){if(!v.called){v.called=!0;var a=document.querySelectorAll("pre code");G.forEach.call(a,I)}}function A(a){a=(a||"").toLowerCase();
-return z[a]||z[F[a]]}var G=[],y=Object.keys,z={},F={},J=/^(no-?highlight|plain|text)$/i,O=/\blang(?:uage)?-([\w-]+)\b/i,N=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,w={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=B;b.highlightAuto=E;b.fixMarkup=H;b.highlightBlock=I;b.configure=function(a){w=n(w,a)};b.initHighlighting=v;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",v,!1);addEventListener("load",v,!1)};b.registerLanguage=function(a,c){c=z[a]=c(b);c.aliases&&
-c.aliases.forEach(function(c){F[c]=a})};b.listLanguages=function(){return y(z)};b.getLanguage=A;b.inherit=n;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE=
-{begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};b.COMMENT=function(a,c,d){a=b.inherit({className:"comment",begin:a,end:c,contains:[]},d||{});
-a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#","$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",
-begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};
-b.registerLanguage("1c",function(a){var c=a.inherit(a.NUMBER_MODE),b={className:"string",begin:'"|\\|',end:'"|$',contains:[{begin:'""'}]},e={begin:"'",end:"'",excludeBegin:!0,excludeEnd:!0,contains:[{className:"number",begin:"\\d{4}([\\.\\\\/:-]?\\d{2}){0,5}"}]},f=a.inherit(a.C_LINE_COMMENT_MODE),g={className:"meta",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",begin:"#|&",end:"$",keywords:{"meta-keyword":"\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u0432\u0435\u0431\u043a\u043b\u0438\u0435\u043d\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u0435\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a\u043e\u043d\u0435\u0446\u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043b\u0438\u0435\u043d\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434 \u043f\u043e\u0441\u043b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0431\u044b\u0447\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043d\u043a\u0438\u0439\u043a\u043b\u0438\u0435\u043d\u0442 "},
+ highlight.js v9.14.0 | BSD3 License | git.io/hljslicense */
+var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(b){var l=0;return function(){return l<b.length?{done:!1,value:b[l++]}:{done:!0}}};$jscomp.arrayIterator=function(b){return{next:$jscomp.arrayIteratorImpl(b)}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
+$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,l,t){b!=Array.prototype&&b!=Object.prototype&&(b[l]=t.value)};$jscomp.getGlobal=function(b){return"undefined"!=typeof window&&window===b?b:"undefined"!=typeof global&&null!=global?global:b};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_";$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};
+$jscomp.Symbol=function(){var b=0;return function(l){return $jscomp.SYMBOL_PREFIX+(l||"")+b++}}();$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.iterator;b||(b=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[b]&&$jscomp.defineProperty(Array.prototype,b,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}});$jscomp.initSymbolIterator=function(){}};
+$jscomp.initSymbolAsyncIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.asyncIterator;b||(b=$jscomp.global.Symbol.asyncIterator=$jscomp.global.Symbol("asyncIterator"));$jscomp.initSymbolAsyncIterator=function(){}};$jscomp.iteratorPrototype=function(b){$jscomp.initSymbolIterator();b={next:b};b[$jscomp.global.Symbol.iterator]=function(){return this};return b};
+$jscomp.iteratorFromArray=function(b,l){$jscomp.initSymbolIterator();b instanceof String&&(b+="");var t=0,q={next:function(){if(t<b.length){var u=t++;return{value:l(u,b[u]),done:!1}}q.next=function(){return{done:!0,value:void 0}};return q.next()}};q[Symbol.iterator]=function(){return q};return q};
+$jscomp.polyfill=function(b,l,t,q){if(l){t=$jscomp.global;b=b.split(".");for(q=0;q<b.length-1;q++){var u=b[q];u in t||(t[u]={});t=t[u]}b=b[b.length-1];q=t[b];l=l(q);l!=q&&null!=l&&$jscomp.defineProperty(t,b,{configurable:!0,writable:!0,value:l})}};$jscomp.polyfill("Array.prototype.keys",function(b){return b?b:function(){return $jscomp.iteratorFromArray(this,function(b){return b})}},"es6","es3");
+(function(b){var l="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):l&&(l.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return l.hljs}))})(function(b){function l(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function t(a,c){return(a=a&&a.exec(c))&&0===a.index}function q(a){var c,d={},b=Array.prototype.slice.call(arguments,1);for(c in a)d[c]=a[c];b.forEach(function(a){for(c in a)d[c]=a[c]});
+return d}function u(a){var c=[];(function g(a,b){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?b+=a.nodeValue.length:1===a.nodeType&&(c.push({event:"start",offset:b,node:a}),b=g(a,b),a.nodeName.toLowerCase().match(/br|hr|img|input/)||c.push({event:"stop",offset:b,node:a}));return b})(a,0);return c}function L(a,c,b){function d(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function f(a){p+="<"+a.nodeName.toLowerCase()+G.map.call(a.attributes,
+function(a){return" "+a.nodeName+'="'+l(a.value).replace('"',"&quot;")+'"'}).join("")+">"}function g(a){p+="</"+a.nodeName.toLowerCase()+">"}function k(a){("start"===a.event?f:g)(a.node)}for(var m=0,p="",n=[];a.length||c.length;){var h=d();p+=l(b.substring(m,h[0].offset));m=h[0].offset;if(h===a){n.reverse().forEach(g);do k(h.splice(0,1)[0]),h=d();while(h===a&&h.length&&h[0].offset===m);n.reverse().forEach(f)}else"start"===h[0].event?n.push(h[0].node):n.pop(),k(h.splice(0,1)[0])}return p+l(b.substr(m))}
+function M(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(c){return q(a,{variants:null},c)}));return a.cached_variants||a.endsWithParent&&[q(a)]||[a]}function N(a){function c(a){return a&&a.source||a}function b(b,d){return new RegExp(c(b),"m"+(a.case_insensitive?"i":"")+(d?"g":""))}function e(d,g){if(!d.compiled){d.compiled=!0;d.keywords=d.keywords||d.beginKeywords;if(d.keywords){var f={},m=function(c,d){a.case_insensitive&&(d=d.toLowerCase());d.split(" ").forEach(function(a){a=
+a.split("|");f[a[0]]=[c,a[1]?Number(a[1]):1]})};"string"===typeof d.keywords?m("keyword",d.keywords):y(d.keywords).forEach(function(a){m(a,d.keywords[a])});d.keywords=f}d.lexemesRe=b(d.lexemes||/\w+/,!0);g&&(d.beginKeywords&&(d.begin="\\b("+d.beginKeywords.split(" ").join("|")+")\\b"),d.begin||(d.begin=/\B|\b/),d.beginRe=b(d.begin),d.endSameAsBegin&&(d.end=d.begin),d.end||d.endsWithParent||(d.end=/\B|\b/),d.end&&(d.endRe=b(d.end)),d.terminator_end=c(d.end)||"",d.endsWithParent&&g.terminator_end&&
+(d.terminator_end+=(d.end?"|":"")+g.terminator_end));d.illegal&&(d.illegalRe=b(d.illegal));null==d.relevance&&(d.relevance=1);d.contains||(d.contains=[]);d.contains=Array.prototype.concat.apply([],d.contains.map(function(a){return M("self"===a?d:a)}));d.contains.forEach(function(a){e(a,d)});d.starts&&e(d.starts,g);g=d.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([d.terminator_end,d.illegal]).map(c).filter(Boolean);d.terminators=g.length?b(g.join("|"),!0):
+{exec:function(){return null}}}}e(a)}function B(a,c,d,b){function e(a,c){if(t(a.endRe,c)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return e(a.parent,c)}function g(a,c,d,b){return'<span class="'+(b?"":w.classPrefix)+(a+'">')+c+(d?"":"</span>")}function k(){var a=x,c;if(null!=h.subLanguage)if((c="string"===typeof h.subLanguage)&&!z[h.subLanguage])c=l(r);else{var d=c?B(h.subLanguage,r,!0,q[h.subLanguage]):E(r,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(u+=d.relevance);
+c&&(q[h.subLanguage]=d.top);c=g(d.language,d.value,!1,!0)}else if(h.keywords){d="";var b=0;h.lexemesRe.lastIndex=0;for(c=h.lexemesRe.exec(r);c;){d+=l(r.substring(b,c.index));b=h;var e=c;e=n.case_insensitive?e[0].toLowerCase():e[0];(b=b.keywords.hasOwnProperty(e)&&b.keywords[e])?(u+=b[1],d+=g(b[0],l(c[0]))):d+=l(c[0]);b=h.lexemesRe.lastIndex;c=h.lexemesRe.exec(r)}c=d+l(r.substr(b))}else c=l(r);x=a+c;r=""}function m(a){x+=a.className?g(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function p(a,
+c){r+=a;if(null==c)return k(),0;a:{a=h;var b;var f=0;for(b=a.contains.length;f<b;f++)if(t(a.contains[f].beginRe,c)){a.contains[f].endSameAsBegin&&(a.contains[f].endRe=new RegExp(a.contains[f].beginRe.exec(c)[0].replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&"),"m"));a=a.contains[f];break a}a=void 0}if(a)return a.skip?r+=c:(a.excludeBegin&&(r+=c),k(),a.returnBegin||a.excludeBegin||(r=c)),m(a,c),a.returnBegin?0:c.length;if(a=e(h,c)){f=h;f.skip?r+=c:(f.returnEnd||f.excludeEnd||(r+=c),k(),f.excludeEnd&&(r=c));
+do h.className&&(x+="</span>"),h.skip||h.subLanguage||(u+=h.relevance),h=h.parent;while(h!==a.parent);a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),m(a.starts,""));return f.returnEnd?0:c.length}if(!d&&t(h.illegalRe,c))throw Error('Illegal lexeme "'+c+'" for mode "'+(h.className||"<unnamed>")+'"');r+=c;return c.length||1}var n=A(a);if(!n)throw Error('Unknown language: "'+a+'"');N(n);var h=b||n,q={},x="";for(b=h;b!==n;b=b.parent)b.className&&(x=g(b.className,"",!0)+x);var r="",u=0;try{for(var v,
+y,C=0;;){h.terminators.lastIndex=C;v=h.terminators.exec(c);if(!v)break;y=p(c.substring(C,v.index),v[0]);C=v.index+y}p(c.substr(C));for(b=h;b.parent;b=b.parent)b.className&&(x+="</span>");return{relevance:u,value:x,language:a,top:h}}catch(D){if(D.message&&-1!==D.message.indexOf("Illegal"))return{relevance:0,value:l(c)};throw D;}}function E(a,c){c=c||w.languages||y(z);var d={relevance:0,value:l(a)},b=d;c.filter(A).filter(H).forEach(function(c){var e=B(c,a,!1);e.language=c;e.relevance>b.relevance&&(b=
+e);e.relevance>d.relevance&&(b=d,d=e)});b.language&&(d.second_best=b);return d}function I(a){return w.tabReplace||w.useBR?a.replace(O,function(a,d){return w.useBR&&"\n"===a?"<br>":w.tabReplace?d.replace(/\t/g,w.tabReplace):""}):a}function J(a){var c,d;a:{var b=a.className+" ";b+=a.parentNode?a.parentNode.className:"";if(d=P.exec(b))d=A(d[1])?d[1]:"no-highlight";else{b=b.split(/\s+/);d=0;for(c=b.length;d<c;d++){var f=b[d];if(K.test(f)||A(f)){d=f;break a}}d=void 0}}if(!K.test(d)){w.useBR?(f=document.createElementNS("http://www.w3.org/1999/xhtml",
+"div"),f.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):f=a;c=f.textContent;b=d?B(d,c,!0):E(c);f=u(f);if(f.length){var g=document.createElementNS("http://www.w3.org/1999/xhtml","div");g.innerHTML=b.value;b.value=L(f,u(g),c)}b.value=I(b.value);a.innerHTML=b.value;c=a.className;d=d?F[d]:b.language;f=[c.trim()];c.match(/\bhljs\b/)||f.push("hljs");-1===c.indexOf(d)&&f.push(d);d=f.join(" ").trim();a.className=d;a.result={language:b.language,re:b.relevance};b.second_best&&(a.second_best=
+{language:b.second_best.language,re:b.second_best.relevance})}}function v(){if(!v.called){v.called=!0;var a=document.querySelectorAll("pre code");G.forEach.call(a,J)}}function A(a){a=(a||"").toLowerCase();return z[a]||z[F[a]]}function H(a){return(a=A(a))&&!a.disableAutodetect}var G=[],y=Object.keys,z={},F={},K=/^(no-?highlight|plain|text)$/i,P=/\blang(?:uage)?-([\w-]+)\b/i,O=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,w={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=B;b.highlightAuto=
+E;b.fixMarkup=I;b.highlightBlock=J;b.configure=function(a){w=q(w,a)};b.initHighlighting=v;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",v,!1);addEventListener("load",v,!1)};b.registerLanguage=function(a,c){c=z[a]=c(b);c.aliases&&c.aliases.forEach(function(c){F[c]=a})};b.listLanguages=function(){return y(z)};b.getLanguage=A;b.autoDetection=H;b.inherit=q;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";
+b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};
+b.COMMENT=function(a,c,d){a=b.inherit({className:"comment",begin:a,end:c,contains:[]},d||{});a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#","$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE=
+{className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,
+relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};b.registerLanguage("1c",function(a){var c=a.inherit(a.NUMBER_MODE),b={className:"string",begin:'"|\\|',end:'"|$',contains:[{begin:'""'}]},e={begin:"'",end:"'",excludeBegin:!0,excludeEnd:!0,contains:[{className:"number",begin:"\\d{4}([\\.\\\\/:-]?\\d{2}){0,5}"}]},f=a.inherit(a.C_LINE_COMMENT_MODE),g={className:"meta",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",
+begin:"#|&",end:"$",keywords:{"meta-keyword":"\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c\u0438\u0437\u0444\u0430\u0439\u043b\u0430 \u0432\u0435\u0431\u043a\u043b\u0438\u0435\u043d\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u0435\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a\u043e\u043d\u0435\u0446\u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043a\u043b\u0438\u0435\u043d\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u043a\u043b\u0438\u0435\u043d\u0442\u0435\u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043d\u0430\u0441\u0435\u0440\u0432\u0435\u0440\u0435\u0431\u0435\u0437\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434 \u043f\u043e\u0441\u043b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0431\u044b\u0447\u043d\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043b\u0441\u0442\u044b\u0439\u043a\u043b\u0438\u0435\u043d\u0442\u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u0435\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u043e\u043d\u043a\u0438\u0439\u043a\u043b\u0438\u0435\u043d\u0442 "},
 contains:[f]};a={className:"function",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",variants:[{begin:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430|\u0444\u0443\u043d\u043a\u0446\u0438\u044f",end:"\\)",keywords:"\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f"},{begin:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b|\u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438",
 keywords:"\u043a\u043e\u043d\u0435\u0446\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u043a\u043e\u043d\u0435\u0446\u0444\u0443\u043d\u043a\u0446\u0438\u0438"}],contains:[{begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"params",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",end:",",excludeEnd:!0,endsWithParent:!0,
 keywords:{keyword:"\u0437\u043d\u0430\u0447",literal:"null \u0438\u0441\u0442\u0438\u043d\u0430 \u043b\u043e\u0436\u044c \u043d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e"},contains:[c,b,e]},f]},a.inherit(a.TITLE_MODE,{begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+"})]};return{case_insensitive:!0,lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]+",keywords:{keyword:"\u0434\u0430\u043b\u0435\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c\u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u043b\u044f \u0435\u0441\u043b\u0438 \u0438 \u0438\u0437 \u0438\u043b\u0438 \u0438\u043d\u0430\u0447\u0435 \u0438\u043d\u0430\u0447\u0435\u0435\u0441\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0446\u0446\u0438\u043a\u043b\u0430 \u043d\u0435 \u043d\u043e\u0432\u044b\u0439 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u0435\u0440\u0435\u043c \u043f\u043e \u043f\u043e\u043a\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0442\u043e\u0433\u0434\u0430 \u0446\u0438\u043a\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442 ",
@@ -38,27 +40,34 @@
 endsParent:!0},{className:"keyword",beginKeywords:"not null constant access function procedure in out aliased exception"},{className:"type",begin:"[A-Za-z](_?[A-Za-z0-9.])*",endsParent:!0,relevance:0}]};return{case_insensitive:!0,keywords:{keyword:"abort else new return abs elsif not reverse abstract end accept entry select access exception of separate aliased exit or some all others subtype and for out synchronized array function overriding at tagged generic package task begin goto pragma terminate body private then if procedure type case in protected constant interface is raise use declare range delay limited record when delta loop rem while digits renames with do mod requeue xor",
 literal:"True False"},contains:[a,{className:"string",begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{className:"string",begin:/'.'/},{className:"number",begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",relevance:0},{className:"symbol",begin:"'[A-Za-z](_?[A-Za-z0-9.])*"},{className:"title",begin:"(\\bwith\\s+)?(\\bprivate\\s+)?\\bpackage\\s+(\\bbody\\s+)?",end:"(is|$)",keywords:"package body",excludeBegin:!0,excludeEnd:!0,
 illegal:"[]{}%#'\""},{begin:"(\\b(with|overriding)\\s+)?\\b(function|procedure)\\s+",end:"(\\bis|\\bwith|\\brenames|\\)\\s*;)",keywords:"overriding function procedure with is renames return",returnBegin:!0,contains:[a,{className:"title",begin:"(\\bwith\\s+)?\\b(function|procedure)\\s+",end:"(\\(|\\s+|$)",excludeBegin:!0,excludeEnd:!0,illegal:"[]{}%#'\""},c,{className:"type",begin:"\\breturn\\s+",end:"(\\s+|;|$)",keywords:"return",excludeBegin:!0,excludeEnd:!0,endsParent:!0,illegal:"[]{}%#'\""}]},
-{className:"type",begin:"\\b(sub)?type\\s+",end:"\\s+",keywords:"type",excludeBegin:!0,illegal:"[]{}%#'\""},c]}});b.registerLanguage("apache",function(a){var c={className:"number",begin:"[\\$%]\\d+"};return{aliases:["apacheconf"],case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,{className:"section",begin:"</?",end:">"},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},
-starts:{end:/$/,relevance:0,keywords:{literal:"on off all"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",c]},c,a.QUOTE_STRING_MODE]}}],illegal:/\S/}});b.registerLanguage("applescript",function(a){var c=a.inherit(a.QUOTE_STRING_MODE,{illegal:""}),b={className:"params",begin:"\\(",end:"\\)",contains:["self",a.C_NUMBER_MODE,c]},e=a.COMMENT("--","$"),f=a.COMMENT("\\(\\*","\\*\\)",{contains:["self",e]});return{aliases:["osascript"],
-keywords:{keyword:"about above after against and around as at back before beginning behind below beneath beside between but by considering contain contains continue copy div does eighth else end equal equals error every exit fifth first for fourth from front get given global if ignoring in into is it its last local me middle mod my ninth not of on onto or over prop property put ref reference repeat returning script second set seventh since sixth some tell tenth that the|0 then third through thru timeout times to transaction try until where while whose with without",
+{className:"type",begin:"\\b(sub)?type\\s+",end:"\\s+",keywords:"type",excludeBegin:!0,illegal:"[]{}%#'\""},c]}});b.registerLanguage("angelscript",function(a){var c={className:"built_in",begin:"\\b(void|bool|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|string|ref|array|double|float|auto|dictionary)"},b={className:"symbol",begin:"[a-zA-Z0-9_]+@"},e={className:"keyword",begin:"<",end:">",contains:[c,b]};c.contains=[e];b.contains=[e];return{aliases:["asc"],keywords:"for in|0 break continue while do|0 return if else case switch namespace is cast or and xor not get|0 in inout|10 out override set|0 private public const default|0 final shared external mixin|10 enum typedef funcdef this super import from interface abstract|0 try catch protected explicit",
+illegal:"(^using\\s+[A-Za-z0-9_\\.]+;$|\\bfunctions*[^\\(])",contains:[{className:"string",begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE],relevance:0},{className:"string",begin:'"""',end:'"""'},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{beginKeywords:"interface namespace",end:"{",illegal:"[;.\\-]",contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+"}]},{beginKeywords:"class",end:"{",illegal:"[;.\\-]",
+contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+",contains:[{begin:"[:,]\\s*",contains:[{className:"symbol",begin:"[a-zA-Z0-9_]+"}]}]}]},c,b,{className:"literal",begin:"\\b(null|true|false)"},{className:"number",begin:"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?f?|\\.\\d+f?)([eE][-+]?\\d+f?)?)"}]}});b.registerLanguage("apache",function(a){var c={className:"number",begin:"[\\$%]\\d+"};return{aliases:["apacheconf"],case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,{className:"section",begin:"</?",
+end:">"},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",c]},c,a.QUOTE_STRING_MODE]}}],illegal:/\S/}});b.registerLanguage("applescript",function(a){var c=
+a.inherit(a.QUOTE_STRING_MODE,{illegal:""}),b={className:"params",begin:"\\(",end:"\\)",contains:["self",a.C_NUMBER_MODE,c]},e=a.COMMENT("--","$"),f=a.COMMENT("\\(\\*","\\*\\)",{contains:["self",e]});return{aliases:["osascript"],keywords:{keyword:"about above after against and around as at back before beginning behind below beneath beside between but by considering contain contains continue copy div does eighth else end equal equals error every exit fifth first for fourth from front get given global if ignoring in into is it its last local me middle mod my ninth not of on onto or over prop property put ref reference repeat returning script second set seventh since sixth some tell tenth that the|0 then third through thru timeout times to transaction try until where while whose with without",
 literal:"AppleScript false linefeed return pi quote result space tab true",built_in:"alias application boolean class constant date file integer list number real record string text activate beep count delay launch log offset read round run say summarize write character characters contents day frontmost id item length month name paragraph paragraphs rest reverse running time version weekday word words year"},contains:[c,a.C_NUMBER_MODE,{className:"built_in",begin:"\\b(clipboard info|the clipboard|info for|list (disks|folder)|mount volume|path to|(close|open for) access|(get|set) eof|current date|do shell script|get volume settings|random number|set volume|system attribute|system info|time to GMT|(load|run|store) script|scripting components|ASCII (character|number)|localized string|choose (application|color|file|file name|folder|from list|remote application|URL)|display (alert|dialog))\\b|^\\s*return\\b"},
 {className:"literal",begin:"\\b(text item delimiters|current application|missing value)\\b"},{className:"keyword",begin:"\\b(apart from|aside from|instead of|out of|greater than|isn't|(doesn't|does not) (equal|come before|come after|contain)|(greater|less) than( or equal)?|(starts?|ends|begins?) with|contained by|comes (before|after)|a (ref|reference)|POSIX file|POSIX path|(date|time) string|quoted form)\\b"},{beginKeywords:"on",illegal:"[${=;\\n]",contains:[a.UNDERSCORE_TITLE_MODE,b]}].concat([e,
-f,a.HASH_COMMENT_MODE]),illegal:"//|->|=>|\\[\\["}});b.registerLanguage("cpp",function(a){var c={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},b={className:"string",variants:[{begin:'(u8?|U)?L?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},e={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],
-relevance:0},f={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},g=a.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",
+f,a.HASH_COMMENT_MODE]),illegal:"//|->|=>|\\[\\["}});b.registerLanguage("arcade",function(a){var c={keyword:"if for while var new function do return void else break",literal:"true false null undefined NaN Infinity PI BackSlash DoubleQuote ForwardSlash NewLine SingleQuote Tab",built_in:"Abs Acos Area AreaGeodetic Asin Atan Atan2 Average Boolean Buffer BufferGeodetic Ceil Centroid Clip Console Constrain Contains Cos Count Crosses Cut Date DateAdd DateDiff Day Decode DefaultValue Dictionary Difference Disjoint Distance Distinct DomainCode DomainName Equals Exp Extent Feature FeatureSet FeatureSetById FeatureSetByTitle FeatureSetByUrl Filter First Floor Geometry Guid HasKey Hour IIf IndexOf Intersection Intersects IsEmpty Length LengthGeodetic Log Max Mean Millisecond Min Minute Month MultiPartToSinglePart Multipoint NextSequenceValue Now Number OrderBy Overlaps Point Polygon Polyline Pow Random Relate Reverse Round Second SetGeometry Sin Sort Sqrt Stdev Sum SymmetricDifference Tan Text Timestamp Today ToLocal Top Touches ToUTC TypeOf Union Variance Weekday When Within Year "},
+b={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:c,contains:[]},f={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,b,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["arcade"],keywords:c,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,a.C_LINE_COMMENT_MODE,
+a.C_BLOCK_COMMENT_MODE,{className:"symbol",begin:"\\$[feature|layer|map|value|view]+"},b,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z_][0-9A-Za-z_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z_][0-9A-Za-z_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(return)\\b)\\s*",keywords:"return",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z_][0-9A-Za-z_]*)\\s*=>",returnBegin:!0,end:"\\s*=>",
+contains:[{className:"params",variants:[{begin:"[A-Za-z_][0-9A-Za-z_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,contains:e}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_][0-9A-Za-z_]*"}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/}],illegal:/#(?!!)/}});b.registerLanguage("cpp",function(a){var c={className:"keyword",
+begin:"\\b[a-z\\d_]*_t\\b"},b={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U|L)?R"\\(',end:'\\)"'},{begin:"'\\\\?.",end:"'",illegal:"."}]},e={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},f={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
+contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},g=a.IDENT_RE+"\\s*\\(",k={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",
 built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",
-literal:"true false nullptr NULL"},p=[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,b];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:l,illegal:"</",contains:p.concat([f,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:l,contains:["self",c]},{begin:a.IDENT_RE+"::",keywords:l},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
-end:/;/}],keywords:l,contains:p.concat([{begin:/\(/,end:/\)/,keywords:l,contains:p.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&]/,contains:[{begin:g,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,e,c]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
-f]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:f,strings:b,keywords:l}}});b.registerLanguage("arduino",function(a){var c=a.getLanguage("cpp").exports;return{keywords:{keyword:"boolean byte word string String array "+c.keywords.keyword,built_in:"setup loop while catch for if do goto try switch case else default break continue return KeyboardController MouseController SoftwareSerial EthernetServer EthernetClient LiquidCrystal RobotControl GSMVoiceCall EthernetUDP EsploraTFT HttpClient RobotMotor WiFiClient GSMScanner FileSystem Scheduler GSMServer YunClient YunServer IPAddress GSMClient GSMModem Keyboard Ethernet Console GSMBand Esplora Stepper Process WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage Client Server GSMPIN FileIO Bridge Serial EEPROM Stream Mouse Audio Servo File Task GPRS WiFi Wire TFT GSM SPI SD runShellCommandAsynchronously analogWriteResolution retrieveCallingNumber printFirmwareVersion analogReadResolution sendDigitalPortPair noListenOnLocalhost readJoystickButton setFirmwareVersion readJoystickSwitch scrollDisplayRight getVoiceCallStatus scrollDisplayLeft writeMicroseconds delayMicroseconds beginTransmission getSignalStrength runAsynchronously getAsynchronously listenOnLocalhost getCurrentCarrier readAccelerometer messageAvailable sendDigitalPorts lineFollowConfig countryNameWrite runShellCommand readStringUntil rewindDirectory readTemperature setClockDivider readLightSensor endTransmission analogReference detachInterrupt countryNameRead attachInterrupt encryptionType readBytesUntil robotNameWrite readMicrophone robotNameRead cityNameWrite userNameWrite readJoystickY readJoystickX mouseReleased openNextFile scanNetworks noInterrupts digitalWrite beginSpeaker mousePressed isActionDone mouseDragged displayLogos noAutoscroll addParameter remoteNumber getModifiers keyboardRead userNameRead waitContinue processInput parseCommand printVersion readNetworks writeMessage blinkVersion cityNameRead readMessage setDataMode parsePacket isListening setBitOrder beginPacket isDirectory motorsWrite drawCompass digitalRead clearScreen serialEvent rightToLeft setTextSize leftToRight requestFrom keyReleased compassRead analogWrite interrupts WiFiServer disconnect playMelody parseFloat autoscroll getPINUsed setPINUsed setTimeout sendAnalog readSlider analogRead beginWrite createChar motorsStop keyPressed tempoWrite readButton subnetMask debugPrint macAddress writeGreen randomSeed attachGPRS readString sendString remotePort releaseAll mouseMoved background getXChange getYChange answerCall getResult voiceCall endPacket constrain getSocket writeJSON getButton available connected findUntil readBytes exitValue readGreen writeBlue startLoop IPAddress isPressed sendSysex pauseMode gatewayIP setCursor getOemKey tuneWrite noDisplay loadImage switchPIN onRequest onReceive changePIN playFile noBuffer parseInt overflow checkPIN knobRead beginTFT bitClear updateIR bitWrite position writeRGB highByte writeRed setSpeed readBlue noStroke remoteIP transfer shutdown hangCall beginSMS endWrite attached maintain noCursor checkReg checkPUK shiftOut isValid shiftIn pulseIn connect println localIP pinMode getIMEI display noBlink process getBand running beginSD drawBMP lowByte setBand release bitRead prepare pointTo readRed setMode noFill remove listen stroke detach attach noTone exists buffer height bitSet circle config cursor random IRread setDNS endSMS getKey micros millis begin print write ready flush width isPIN blink clear press mkdir rmdir close point yield image BSSID click delay read text move peek beep rect line open seek fill size turn stop home find step tone sqrt RSSI SSID end bit tan cos sin pow map abs max min get run put",
+literal:"true false nullptr NULL"},m=[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,b];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:k,illegal:"</",contains:m.concat([f,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:k,contains:["self",c]},{begin:a.IDENT_RE+"::",keywords:k},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
+end:/;/}],keywords:k,contains:m.concat([{begin:/\(/,end:/\)/,keywords:k,contains:m.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:k,illegal:/[^\w\s\*&]/,contains:[{begin:g,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,e,c,{begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:["self",
+a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,e,c]}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,f]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:f,strings:b,keywords:k}}});b.registerLanguage("arduino",function(a){var c=a.getLanguage("cpp").exports;return{keywords:{keyword:"boolean byte word string String array "+c.keywords.keyword,built_in:"setup loop while catch for if do goto try switch case else default break continue return KeyboardController MouseController SoftwareSerial EthernetServer EthernetClient LiquidCrystal RobotControl GSMVoiceCall EthernetUDP EsploraTFT HttpClient RobotMotor WiFiClient GSMScanner FileSystem Scheduler GSMServer YunClient YunServer IPAddress GSMClient GSMModem Keyboard Ethernet Console GSMBand Esplora Stepper Process WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage Client Server GSMPIN FileIO Bridge Serial EEPROM Stream Mouse Audio Servo File Task GPRS WiFi Wire TFT GSM SPI SD runShellCommandAsynchronously analogWriteResolution retrieveCallingNumber printFirmwareVersion analogReadResolution sendDigitalPortPair noListenOnLocalhost readJoystickButton setFirmwareVersion readJoystickSwitch scrollDisplayRight getVoiceCallStatus scrollDisplayLeft writeMicroseconds delayMicroseconds beginTransmission getSignalStrength runAsynchronously getAsynchronously listenOnLocalhost getCurrentCarrier readAccelerometer messageAvailable sendDigitalPorts lineFollowConfig countryNameWrite runShellCommand readStringUntil rewindDirectory readTemperature setClockDivider readLightSensor endTransmission analogReference detachInterrupt countryNameRead attachInterrupt encryptionType readBytesUntil robotNameWrite readMicrophone robotNameRead cityNameWrite userNameWrite readJoystickY readJoystickX mouseReleased openNextFile scanNetworks noInterrupts digitalWrite beginSpeaker mousePressed isActionDone mouseDragged displayLogos noAutoscroll addParameter remoteNumber getModifiers keyboardRead userNameRead waitContinue processInput parseCommand printVersion readNetworks writeMessage blinkVersion cityNameRead readMessage setDataMode parsePacket isListening setBitOrder beginPacket isDirectory motorsWrite drawCompass digitalRead clearScreen serialEvent rightToLeft setTextSize leftToRight requestFrom keyReleased compassRead analogWrite interrupts WiFiServer disconnect playMelody parseFloat autoscroll getPINUsed setPINUsed setTimeout sendAnalog readSlider analogRead beginWrite createChar motorsStop keyPressed tempoWrite readButton subnetMask debugPrint macAddress writeGreen randomSeed attachGPRS readString sendString remotePort releaseAll mouseMoved background getXChange getYChange answerCall getResult voiceCall endPacket constrain getSocket writeJSON getButton available connected findUntil readBytes exitValue readGreen writeBlue startLoop IPAddress isPressed sendSysex pauseMode gatewayIP setCursor getOemKey tuneWrite noDisplay loadImage switchPIN onRequest onReceive changePIN playFile noBuffer parseInt overflow checkPIN knobRead beginTFT bitClear updateIR bitWrite position writeRGB highByte writeRed setSpeed readBlue noStroke remoteIP transfer shutdown hangCall beginSMS endWrite attached maintain noCursor checkReg checkPUK shiftOut isValid shiftIn pulseIn connect println localIP pinMode getIMEI display noBlink process getBand running beginSD drawBMP lowByte setBand release bitRead prepare pointTo readRed setMode noFill remove listen stroke detach attach noTone exists buffer height bitSet circle config cursor random IRread setDNS endSMS getKey micros millis begin print write ready flush width isPIN blink clear press mkdir rmdir close point yield image BSSID click delay read text move peek beep rect line open seek fill size turn stop home find step tone sqrt RSSI SSID end bit tan cos sin pow map abs max min get run put",
 literal:"DIGITAL_MESSAGE FIRMATA_STRING ANALOG_MESSAGE REPORT_DIGITAL REPORT_ANALOG INPUT_PULLUP SET_PIN_MODE INTERNAL2V56 SYSTEM_RESET LED_BUILTIN INTERNAL1V1 SYSEX_START INTERNAL EXTERNAL DEFAULT OUTPUT INPUT HIGH LOW"},contains:[c.preprocessor,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("armasm",function(a){return{case_insensitive:!0,aliases:["arm"],lexemes:"\\.?"+a.IDENT_RE,keywords:{meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",
 built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},
 contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?",
 end:"\\s"},a.COMMENT("[;@]","$",{relevance:0}),a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"[=#]\\w+"}],relevance:0}]}});
 b.registerLanguage("xml",function(a){var c={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--",
-"--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[c],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[c],starts:{end:"\x3c/script>",returnEnd:!0,
-subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}});b.registerLanguage("asciidoc",function(a){return{aliases:["adoc"],contains:[a.COMMENT("^/{4,}\\n","\\n/{4,}$",{relevance:10}),a.COMMENT("^//","$",{relevance:0}),{className:"title",begin:"^\\.\\w.*$"},{begin:"^[=\\*]{4,}\\n",end:"\\n^[=\\*]{4,}$",relevance:10},{className:"section",relevance:10,variants:[{begin:"^(={1,5}) .+?( \\1)?$"},
-{begin:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$"}]},{className:"meta",begin:"^:.+?:",end:"\\s",excludeEnd:!0,relevance:10},{className:"meta",begin:"^\\[.+?\\]$",relevance:0},{className:"quote",begin:"^_{4,}\\n",end:"\\n_{4,}$",relevance:10},{className:"code",begin:"^[\\-\\.]{4,}\\n",end:"\\n[\\-\\.]{4,}$",relevance:10},{begin:"^\\+{4,}\\n",end:"\\n\\+{4,}$",contains:[{begin:"<",end:">",subLanguage:"xml",relevance:0}],relevance:10},{className:"bullet",begin:"^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+"},{className:"symbol",
-begin:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",relevance:10},{className:"strong",begin:"\\B\\*(?![\\*\\s])",end:"(\\n{2}|\\*)",contains:[{begin:"\\\\*\\w",relevance:0}]},{className:"emphasis",begin:"\\B'(?!['\\s])",end:"(\\n{2}|')",contains:[{begin:"\\\\'\\w",relevance:0}],relevance:0},{className:"emphasis",begin:"_(?![_\\s])",end:"(\\n{2}|_)",relevance:0},{className:"string",variants:[{begin:"``.+?''"},{begin:"`.+?'"}]},{className:"code",begin:"(`.+?`|\\+.+?\\+)",relevance:0},{className:"code",
-begin:"^[ \\t]",end:"$",relevance:0},{begin:"^'{3,}[ \\t]*$",relevance:10},{begin:"(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]",returnBegin:!0,contains:[{begin:"(link|image:?):",relevance:0},{className:"link",begin:"\\w",end:"[^\\[]+",relevance:0},{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0,relevance:0}],relevance:10}]}});b.registerLanguage("aspectj",function(a){return{keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
+"--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},a.inherit(a.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",
+keywords:{name:"style"},contains:[c],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[c],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}});b.registerLanguage("asciidoc",function(a){return{aliases:["adoc"],contains:[a.COMMENT("^/{4,}\\n","\\n/{4,}$",
+{relevance:10}),a.COMMENT("^//","$",{relevance:0}),{className:"title",begin:"^\\.\\w.*$"},{begin:"^[=\\*]{4,}\\n",end:"\\n^[=\\*]{4,}$",relevance:10},{className:"section",relevance:10,variants:[{begin:"^(={1,5}) .+?( \\1)?$"},{begin:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$"}]},{className:"meta",begin:"^:.+?:",end:"\\s",excludeEnd:!0,relevance:10},{className:"meta",begin:"^\\[.+?\\]$",relevance:0},{className:"quote",begin:"^_{4,}\\n",end:"\\n_{4,}$",relevance:10},{className:"code",begin:"^[\\-\\.]{4,}\\n",
+end:"\\n[\\-\\.]{4,}$",relevance:10},{begin:"^\\+{4,}\\n",end:"\\n\\+{4,}$",contains:[{begin:"<",end:">",subLanguage:"xml",relevance:0}],relevance:10},{className:"bullet",begin:"^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+"},{className:"symbol",begin:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",relevance:10},{className:"strong",begin:"\\B\\*(?![\\*\\s])",end:"(\\n{2}|\\*)",contains:[{begin:"\\\\*\\w",relevance:0}]},{className:"emphasis",begin:"\\B'(?!['\\s])",end:"(\\n{2}|')",contains:[{begin:"\\\\'\\w",relevance:0}],
+relevance:0},{className:"emphasis",begin:"_(?![_\\s])",end:"(\\n{2}|_)",relevance:0},{className:"string",variants:[{begin:"``.+?''"},{begin:"`.+?'"}]},{className:"code",begin:"(`.+?`|\\+.+?\\+)",relevance:0},{className:"code",begin:"^[ \\t]",end:"$",relevance:0},{begin:"^'{3,}[ \\t]*$",relevance:10},{begin:"(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]",returnBegin:!0,contains:[{begin:"(link|image:?):",relevance:0},{className:"link",begin:"\\w",end:"[^\\[]+",relevance:0},{className:"string",
+begin:"\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0,relevance:0}],relevance:10}]}});b.registerLanguage("aspectj",function(a){return{keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
 illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"aspect",end:/[{;=]/,excludeEnd:!0,illegal:/[:;"\[\]]/,contains:[{beginKeywords:"extends implements pertypewithin perthis pertarget percflowbelow percflow issingleton"},a.UNDERSCORE_TITLE_MODE,{begin:/\([^\)]*/,end:/[)]+/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance get set args call",
 excludeEnd:!1}]},{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,relevance:0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"pointcut after before around throwing returning",end:/[)]/,excludeEnd:!1,illegal:/["\[\]]/,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.UNDERSCORE_TITLE_MODE]}]},{begin:/[:]/,returnBegin:!0,end:/[{;]/,relevance:0,excludeEnd:!1,keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance",
 illegal:/["\[\]]/,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",keywords:"false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else extends implements break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws privileged aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization staticinitialization withincode target within execution getWithinTypeName handler thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents warning error soft precedence thisAspectInstance get set args call",
@@ -85,10 +94,10 @@
 end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]}]}});b.registerLanguage("ceylon",function(a){var c={className:"subst",excludeBegin:!0,excludeEnd:!0,begin:/``/,end:/``/,keywords:"assembly module package import alias class interface object given value assign void function new of extends satisfies abstracts in out return break continue throw assert dynamic if else switch case for while try catch finally then let this outer super is exists nonempty",
 relevance:10},b=[{className:"string",begin:'"""',end:'"""',relevance:10},{className:"string",begin:'"',end:'"',contains:[c]},{className:"string",begin:"'",end:"'"},{className:"number",begin:"#[0-9a-fA-F_]+|\\$[01_]+|[0-9_]+(?:\\.[0-9_](?:[eE][+-]?\\d+)?)?[kMGTPmunpf]?",relevance:0}];c.contains=b;return{keywords:{keyword:"assembly module package import alias class interface object given value assign void function new of extends satisfies abstracts in out return break continue throw assert dynamic if else switch case for while try catch finally then let this outer super is exists nonempty shared abstract formal default actual variable late native deprecatedfinal sealed annotation suppressWarnings small",
 meta:"doc by license see throws tagged"},illegal:"\\$[^01]|#[^0-9a-fA-F]",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),{className:"meta",begin:'@[a-z]\\w*(?:\\:"[^"]*")?'}].concat(b)}});b.registerLanguage("clean",function(a){return{aliases:["clean","icl","dcl"],keywords:{keyword:"if let in with where case of class instance otherwise implementation definition system module from import qualified as special code inline foreign export ccall stdcall generic derive infix infixl infixr",
-literal:"True False"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{begin:"->|<-[|:]?|::|#!?|>>=|\\{\\||\\|\\}|:==|=:|\\.\\.|<>|`"}]}});b.registerLanguage("clojure",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),f={className:"literal",begin:/\b(true|false|nil)\b/},g={begin:"[\\[\\{]",end:"[\\]\\}]"},l={className:"comment",
-begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},p=a.COMMENT("\\^\\{","\\}"),m={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},u={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},k={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
-lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},n=[u,b,l,p,e,m,g,c,f,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];u.contains=[a.COMMENT("comment",""),k,h];h.contains=n;g.contains=n;p.contains=[g];return{aliases:["clj"],illegal:/\S/,contains:[u,b,l,p,e,m,g,c,f]}});b.registerLanguage("clojure-repl",function(a){return{contains:[{className:"meta",begin:/^([\w.-]+|\s*#_)=>/,
-starts:{end:/$/,subLanguage:"clojure"}}]}});b.registerLanguage("cmake",function(a){return{aliases:["cmake.in"],case_insensitive:!0,keywords:{keyword:"add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_subdirectory add_test aux_source_directory break build_command cmake_minimum_required cmake_policy configure_file create_test_sourcelist define_property else elseif enable_language enable_testing endforeach endfunction endif endmacro endwhile execute_process export find_file find_library find_package find_path find_program fltk_wrap_ui foreach function get_cmake_property get_directory_property get_filename_component get_property get_source_file_property get_target_property get_test_property if include include_directories include_external_msproject include_regular_expression install link_directories load_cache load_command macro mark_as_advanced message option output_required_files project qt_wrap_cpp qt_wrap_ui remove_definitions return separate_arguments set set_directory_properties set_property set_source_files_properties set_target_properties set_tests_properties site_name source_group string target_link_libraries try_compile try_run unset variable_watch while build_name exec_program export_library_dependencies install_files install_programs install_targets link_libraries make_directory remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or equal less greater strless strgreater strequal matches"},
+built_in:"Int Real Char Bool",literal:"True False"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{begin:"->|<-[|:]?|#!?|>>=|\\{\\||\\|\\}|:==|=:|<>"}]}});b.registerLanguage("clojure",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),f={className:"literal",begin:/\b(true|false|nil)\b/},g={begin:"[\\[\\{]",end:"[\\]\\}]"},k=
+{className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},m=a.COMMENT("\\^\\{","\\}"),p={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},l={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
+lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},q=[n,b,k,m,e,p,g,c,f,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),l,h];h.contains=q;g.contains=q;m.contains=[g];return{aliases:["clj"],illegal:/\S/,contains:[n,b,k,m,e,p,g,c,f]}});b.registerLanguage("clojure-repl",function(a){return{contains:[{className:"meta",begin:/^([\w.-]+|\s*#_)?=>/,
+starts:{end:/$/,subLanguage:"clojure"}}]}});b.registerLanguage("cmake",function(a){return{aliases:["cmake.in"],case_insensitive:!0,keywords:{keyword:"break cmake_host_system_information cmake_minimum_required cmake_parse_arguments cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro endwhile execute_process file find_file find_library find_package find_path find_program foreach function get_cmake_property get_directory_property get_filename_component get_property if include include_guard list macro mark_as_advanced math message option return separate_arguments set_directory_properties set_property set site_name string unset variable_watch while add_compile_definitions add_compile_options add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_link_options add_subdirectory add_test aux_source_directory build_command create_test_sourcelist define_property enable_language enable_testing export fltk_wrap_ui get_source_file_property get_target_property get_test_property include_directories include_external_msproject include_regular_expression install link_directories link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions set_source_files_properties set_target_properties set_tests_properties source_group target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_directories target_link_libraries target_link_options target_sources try_compile try_run ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ctest_test ctest_update ctest_upload build_name exec_program export_library_dependencies install_files install_programs install_targets load_command make_directory output_required_files remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or not command policy target test exists is_newer_than is_directory is_symlink is_absolute matches less greater equal less_equal greater_equal strless strgreater strequal strless_equal strgreater_equal version_less version_greater version_equal version_less_equal version_greater_equal in_list defined"},
 contains:[{className:"variable",begin:"\\${",end:"}"},a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE]}});b.registerLanguage("coffeescript",function(a){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},
 b={className:"subst",begin:/#\{/,end:/}/,keywords:c},e=[a.BINARY_NUMBER_MODE,a.inherit(a.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[a.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[a.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[a.BACKSLASH_ESCAPE,b]},{begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[b,a.HASH_COMMENT_MODE]},{begin:"//[gim]*",relevance:0},
 {begin:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];b.contains=e;b=a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"});var f={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:c,contains:["self"].concat(e)}]};return{aliases:["coffee","cson","iced"],keywords:c,illegal:/\/\*/,contains:e.concat([a.COMMENT("###",
@@ -100,19 +109,19 @@
 {begin:/&sql\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,subLanguage:"sql"},{begin:/&(js|jscript|javascript)</,end:/>/,excludeBegin:!0,excludeEnd:!0,subLanguage:"javascript"},{begin:/&html<\s*</,end:/>\s*>/,subLanguage:"xml"}]}});b.registerLanguage("crmsh",function(a){return{aliases:["crm","pcmk"],case_insensitive:!0,keywords:{keyword:"params meta operations op rule attributes utilization read write deny defined not_defined in_range date spec in ref reference attribute type xpath version and or lt gt tag lte gte eq ne \\ number string",
 literal:"Master Started Slave Stopped start promote demote stop monitor true false"},contains:[a.HASH_COMMENT_MODE,{beginKeywords:"node",starts:{end:"\\s*([\\w_-]+:)?",starts:{className:"title",end:"\\s*[\\$\\w_][\\w_-]*"}}},{beginKeywords:"primitive rsc_template",starts:{className:"title",end:"\\s*[\\$\\w_][\\w_-]*",starts:{end:"\\s*@?[\\w_][\\w_\\.:-]*"}}},{begin:"\\b(group|clone|ms|master|location|colocation|order|fencing_topology|rsc_ticket|acl_target|acl_group|user|role|tag|xml)\\s+",keywords:"group clone ms master location colocation order fencing_topology rsc_ticket acl_target acl_group user role tag xml",
 starts:{className:"title",end:"[\\$\\w_][\\w_-]*"}},{beginKeywords:"property rsc_defaults op_defaults",starts:{className:"title",end:"\\s*([\\w_-]+:)?"}},a.QUOTE_STRING_MODE,{className:"meta",begin:"(ocf|systemd|service|lsb):[\\w_:-]+",relevance:0},{className:"number",begin:"\\b\\d+(\\.\\d+)?(ms|s|h|m)?",relevance:0},{className:"literal",begin:"[-]?(infinity|inf)",relevance:0},{className:"attr",begin:/([A-Za-z\$_#][\w_-]+)=/,relevance:0},{className:"tag",begin:"</?",end:"/?>",relevance:0}]}});b.registerLanguage("crystal",
-function(a){function c(a,c){a=[{begin:a,end:c}];return a[0].contains=a}var b={keyword:"abstract alias as as? asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? return require select self sizeof struct super then type typeof union uninitialized unless until when while with yield __DIR__ __END_LINE__ __FILE__ __LINE__",literal:"false nil true"},e={className:"subst",
-begin:"#{",end:"}",keywords:b},f={className:"template-variable",variants:[{begin:"\\{\\{",end:"\\}\\}"},{begin:"\\{%",end:"%\\}"}],keywords:b},g={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%w?\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%w?\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%w?{",end:"}",contains:c("{","}")},{begin:"%w?<",end:">",contains:c("<",">")},{begin:"%w?/",end:"/"},{begin:"%w?%",end:"%"},
-{begin:"%w?-",end:"-"},{begin:"%w?\\|",end:"\\|"},{begin:/<<-\w+$/,end:/^\s*\w+$/}],relevance:0},l={className:"string",variants:[{begin:"%q\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%q\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%q{",end:"}",contains:c("{","}")},{begin:"%q<",end:">",contains:c("<",">")},{begin:"%q/",end:"/"},{begin:"%q%",end:"%"},{begin:"%q-",end:"-"},{begin:"%q\\|",end:"\\|"},{begin:/<<-'\w+'$/,end:/^\s*\w+$/}],relevance:0},p={begin:"(!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~)\\s*",
-contains:[{className:"regexp",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:"//[a-z]*",relevance:0},{begin:"/",end:"/[a-z]*"},{begin:"%r\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%r\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%r{",end:"}",contains:c("{","}")},{begin:"%r<",end:">",contains:c("<",">")},{begin:"%r/",end:"/"},{begin:"%r%",end:"%"},{begin:"%r-",end:"-"},{begin:"%r\\|",end:"\\|"}]}],relevance:0},m={className:"regexp",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:"%r\\(",end:"\\)",
-contains:c("\\(","\\)")},{begin:"%r\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%r{",end:"}",contains:c("{","}")},{begin:"%r<",end:">",contains:c("<",">")},{begin:"%r/",end:"/"},{begin:"%r%",end:"%"},{begin:"%r-",end:"-"},{begin:"%r\\|",end:"\\|"}],relevance:0},k={className:"meta",begin:"@\\[",end:"\\]",contains:[a.inherit(a.QUOTE_STRING_MODE,{className:"meta-string"})]};a=[f,g,l,p,m,k,a.HASH_COMMENT_MODE,{className:"class",beginKeywords:"class module struct",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,
-a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<"}]},{className:"class",beginKeywords:"lib enum union",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],relevance:10},{className:"function",beginKeywords:"def",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\][=?]?",endsParent:!0})]},{className:"function",beginKeywords:"fun macro",
-end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\][=?]?",endsParent:!0})],relevance:5},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\][=?]?"}],relevance:0},{className:"number",variants:[{begin:"\\b0b([01_]*[01])(_[uif](8|16|32|64))?"},{begin:"\\b0o([0-7_]*[0-7])(_[uif](8|16|32|64))?"},
-{begin:"\\b0x([A-Fa-f0-9_]*[A-Fa-f0-9])(_[uif](8|16|32|64))?"},{begin:"\\b(([0-9][0-9_]*[0-9]|[0-9])(\\.[0-9_]*[0-9])?([eE][+-]?[0-9_]*[0-9])?)(_[uif](8|16|32|64))?"}],relevance:0}];e.contains=a;f.contains=a.slice(1);return{aliases:["cr"],lexemes:"[a-zA-Z_]\\w*[!?=]?",keywords:b,contains:a}});b.registerLanguage("cs",function(a){var c={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",
-literal:"null false true"},b={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},e={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},f=a.inherit(e,{illegal:/\n/}),g={className:"subst",begin:"{",end:"}",keywords:c},l=a.inherit(g,{illegal:/\n/}),p={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},
-{begin:"}}"},a.BACKSLASH_ESCAPE,l]},m={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]},k=a.inherit(m,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]});g.contains=[m,p,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,b,a.C_BLOCK_COMMENT_MODE];l.contains=[k,p,f,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,b,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];e={variants:[m,p,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};f=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+
-a.IDENT_RE+")*>)?(\\[\\])?";return{aliases:["csharp"],keywords:c,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},e,b,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,
+function(a){function c(a,c){a=[{begin:a,end:c}];return a[0].contains=a}var b={keyword:"abstract alias annotation as as? asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? return require select self sizeof struct super then type typeof union uninitialized unless until verbatim when while with yield __DIR__ __END_LINE__ __FILE__ __LINE__",literal:"false nil true"},
+e={className:"subst",begin:"#{",end:"}",keywords:b},f={className:"template-variable",variants:[{begin:"\\{\\{",end:"\\}\\}"},{begin:"\\{%",end:"%\\}"}],keywords:b},g={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[Qwi]?\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%[Qwi]?\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%[Qwi]?{",end:"}",contains:c("{","}")},{begin:"%[Qwi]?<",end:">",contains:c("<",">")},{begin:"%[Qwi]?\\|",
+end:"\\|"},{begin:/<<-\w+$/,end:/^\s*\w+$/}],relevance:0},k={className:"string",variants:[{begin:"%q\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%q\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%q{",end:"}",contains:c("{","}")},{begin:"%q<",end:">",contains:c("<",">")},{begin:"%q\\|",end:"\\|"},{begin:/<<-'\w+'$/,end:/^\s*\w+$/}],relevance:0},m={begin:"(?!%})("+a.RE_STARTERS_RE+"|\\n|\\b(case|if|select|unless|until|when|while)\\b)\\s*",keywords:"case if select unless until when while",contains:[{className:"regexp",
+contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:"//[a-z]*",relevance:0},{begin:"/(?!\\/)",end:"/[a-z]*"}]}],relevance:0},p={className:"regexp",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:"%r\\(",end:"\\)",contains:c("\\(","\\)")},{begin:"%r\\[",end:"\\]",contains:c("\\[","\\]")},{begin:"%r{",end:"}",contains:c("{","}")},{begin:"%r<",end:">",contains:c("<",">")},{begin:"%r\\|",end:"\\|"}],relevance:0},n={className:"meta",begin:"@\\[",end:"\\]",contains:[a.inherit(a.QUOTE_STRING_MODE,{className:"meta-string"})]};
+a=[f,g,k,p,m,n,a.HASH_COMMENT_MODE,{className:"class",beginKeywords:"class module struct",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<"}]},{className:"class",beginKeywords:"lib enum union",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],relevance:10},{beginKeywords:"annotation",end:"$|;",illegal:/=/,contains:[a.HASH_COMMENT_MODE,a.inherit(a.TITLE_MODE,
+{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],relevance:10},{className:"function",beginKeywords:"def",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?",endsParent:!0})]},{className:"function",beginKeywords:"fun macro",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?",endsParent:!0})],
+relevance:5},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?"}],relevance:0},{className:"number",variants:[{begin:"\\b0b([01_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b0o([0-7_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b0x([A-Fa-f0-9_]+)(_*[ui](8|16|32|64|128))?"},{begin:"\\b([1-9][0-9_]*[0-9]|[0-9])(\\.[0-9][0-9_]*)?([eE]_*[-+]?[0-9_]*)?(_*f(32|64))?(?!_)"},
+{begin:"\\b([1-9][0-9_]*|0)(_*[ui](8|16|32|64|128))?"}],relevance:0}];e.contains=a;f.contains=a.slice(1);return{aliases:["cr"],lexemes:"[a-zA-Z_]\\w*[!?=]?",keywords:b,contains:a}});b.registerLanguage("cs",function(a){var c={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",
+literal:"null false true"},b={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},e={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},f=a.inherit(e,{illegal:/\n/}),g={className:"subst",begin:"{",end:"}",keywords:c},k=a.inherit(g,{illegal:/\n/}),m={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},
+{begin:"}}"},a.BACKSLASH_ESCAPE,k]},p={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]},n=a.inherit(p,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},k]});g.contains=[p,m,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,b,a.C_BLOCK_COMMENT_MODE];k.contains=[n,m,f,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,b,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];e={variants:[p,m,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};f=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+
+a.IDENT_RE+")*>)?(\\[\\])?";return{aliases:["csharp","c#"],keywords:c,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},e,b,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,
 contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+f+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,
-end:/[{;=]/,excludeEnd:!0,keywords:c,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,relevance:0,contains:[e,b,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("csp",function(a){return{case_insensitive:!1,lexemes:"[a-zA-Z][a-zA-Z0-9_-]*",keywords:{keyword:"base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src media-src object-src plugin-types report-uri sandbox script-src style-src"},
+end:/\s*[{;=]/,excludeEnd:!0,keywords:c,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,relevance:0,contains:[e,b,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("csp",function(a){return{case_insensitive:!1,lexemes:"[a-zA-Z][a-zA-Z0-9_-]*",keywords:{keyword:"base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src media-src object-src plugin-types report-uri sandbox script-src style-src"},
 contains:[{className:"string",begin:"'",end:"'"},{className:"attribute",begin:"^Content",end:":",excludeEnd:!0}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",lexemes:"[a-z-]+",
 keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,
 excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var c=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
@@ -121,40 +130,40 @@
 relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}});b.registerLanguage("markdown",
 function(a){return{aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$"},{begin:"^.+?\\n[=-]{2,}$"}]},{begin:"<",end:">",subLanguage:"xml",relevance:0},{className:"bullet",begin:"^([*+-]|(\\d+\\.))\\s+"},{className:"strong",begin:"[*_]{2}.+?[*_]{2}"},{className:"emphasis",variants:[{begin:"\\*.+?\\*"},{begin:"_.+?_",relevance:0}]},{className:"quote",begin:"^>\\s+",end:"$"},{className:"code",variants:[{begin:"^```w*s*$",end:"^```s*$"},{begin:"`.+?`"},{begin:"^( {4}|\t)",
 end:"$",relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},{begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",
-begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var c={className:"subst",variants:[{begin:"\\${",end:"}"},{begin:"\\$[A-Za-z0-9_]+"}],keywords:"true false null this is new super"},b={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,c]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,c]},{begin:"'",end:"'",illegal:"\\n",
-contains:[a.BACKSLASH_ESCAPE,c]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c]}]};c.contains=[a.C_NUMBER_MODE,b];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},
-contains:[b,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("delphi",function(a){var c=[a.C_LINE_COMMENT_MODE,a.COMMENT(/\{/,/\}/,{relevance:0}),a.COMMENT(/\(\*/,/\*\)/,
-{relevance:10})],b={className:"meta",variants:[{begin:/\{\$/,end:/\}/},{begin:/\(\*\$/,end:/\*\)/}]},e={className:"string",begin:/'/,end:/'/,contains:[{begin:/''/}]},f={className:"string",begin:/(#\d+)+/},g={begin:a.IDENT_RE+"\\s*=\\s*class\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE]},l={className:"function",beginKeywords:"function constructor destructor procedure",end:/[:;]/,keywords:"function constructor|10 destructor|10 procedure|10",contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,
-keywords:"exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",
+begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var c={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"}]},b={className:"subst",variants:[{begin:"\\${",end:"}"}],keywords:"true false null this is new super"};c={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,c,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,
+c,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c,b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c,b]}]};b.contains=[a.C_NUMBER_MODE,c];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",
+built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},contains:[c,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},
+a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("delphi",function(a){var c=[a.C_LINE_COMMENT_MODE,a.COMMENT(/\{/,/\}/,{relevance:0}),a.COMMENT(/\(\*/,/\*\)/,{relevance:10})],b={className:"meta",variants:[{begin:/\{\$/,end:/\}/},{begin:/\(\*\$/,end:/\*\)/}]},e={className:"string",begin:/'/,end:/'/,contains:[{begin:/''/}]},f={className:"string",begin:/(#\d+)+/},g={begin:a.IDENT_RE+"\\s*=\\s*class\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE]},
+k={className:"function",beginKeywords:"function constructor destructor procedure",end:/[:;]/,keywords:"function constructor|10 destructor|10 procedure|10",contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:"exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",
 contains:[e,f,b].concat(c)},b].concat(c)};return{aliases:"dpr dfm pas pascal freepascal lazarus lpr lfm".split(" "),case_insensitive:!0,keywords:"exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure absolute reintroduce operator as is abstract alias assembler bitpacked break continue cppdecl cvar enumerator experimental platform deprecated unimplemented dynamic export far16 forward generic helper implements interrupt iochecks local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat specialize strict unaligned varargs ",
-illegal:/"|\$[G-Zg-z]|\/\*|<\/|\|/,contains:[e,f,a.NUMBER_MODE,g,l,b].concat(c)}});b.registerLanguage("diff",function(a){return{aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{begin:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{begin:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{className:"comment",variants:[{begin:/Index: /,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^\-{3}/,end:/$/},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/\*{5}/,end:/\*{5}$/}]},
+illegal:/"|\$[G-Zg-z]|\/\*|<\/|\|/,contains:[e,f,a.NUMBER_MODE,g,k,b].concat(c)}});b.registerLanguage("diff",function(a){return{aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{begin:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{begin:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{className:"comment",variants:[{begin:/Index: /,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^\-{3}/,end:/$/},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/\*{5}/,end:/\*{5}$/}]},
 {className:"addition",begin:"^\\+",end:"$"},{className:"deletion",begin:"^\\-",end:"$"},{className:"addition",begin:"^\\!",end:"$"}]}});b.registerLanguage("django",function(a){var c={begin:/\|[A-Za-z]+:?/,keywords:{name:"truncatewords removetags linebreaksbr yesno get_digit timesince random striptags filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort dictsortreversed default_if_none pluralize lower join center default truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize localtime utc timezone"},
 contains:[a.QUOTE_STRING_MODE,a.APOS_STRING_MODE]};return{aliases:["jinja"],case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT(/\{%\s*comment\s*%}/,/\{%\s*endcomment\s*%}/),a.COMMENT(/\{#/,/#}/),{className:"template-tag",begin:/\{%/,end:/%}/,contains:[{className:"name",begin:/\w+/,keywords:{name:"comment endcomment load templatetag ifchanged endifchanged if endif firstof for endfor ifnotequal endifnotequal widthratio extends include spaceless endspaceless regroup ifequal endifequal ssi now with cycle url filter endfilter debug block endblock else autoescape endautoescape csrf_token empty elif endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix plural get_current_language language get_available_languages get_current_language_bidi get_language_info get_language_info_list localize endlocalize localtime endlocaltime timezone endtimezone get_current_timezone verbatim"},
 starts:{endsWithParent:!0,keywords:"in by as",contains:[c],relevance:0}}]},{className:"template-variable",begin:/\{\{/,end:/}}/,contains:[c]}]}});b.registerLanguage("dns",function(a){return{aliases:["bind","zone"],keywords:{keyword:"IN A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT"},contains:[a.COMMENT(";","$",{relevance:0}),{className:"meta",begin:/^\$(TTL|GENERATE|INCLUDE|ORIGIN)\b/},
 {className:"number",begin:"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\b"},
-{className:"number",begin:"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b"},a.inherit(a.NUMBER_MODE,{begin:/\b\d+[dhwm]?/})]}});b.registerLanguage("dockerfile",function(a){return{aliases:["docker"],case_insensitive:!0,keywords:"from maintainer expose env arg user onbuild stopsignal",contains:[a.HASH_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE,{beginKeywords:"run cmd entrypoint volume add copy workdir label healthcheck shell",starts:{end:/[^\\]\n/,
+{className:"number",begin:"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b"},a.inherit(a.NUMBER_MODE,{begin:/\b\d+[dhwm]?/})]}});b.registerLanguage("dockerfile",function(a){return{aliases:["docker"],case_insensitive:!0,keywords:"from maintainer expose env arg user onbuild stopsignal",contains:[a.HASH_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE,{beginKeywords:"run cmd entrypoint volume add copy workdir label healthcheck shell",starts:{end:/[^\\]$/,
 subLanguage:"bash"}}],illegal:"</"}});b.registerLanguage("dos",function(a){var c=a.COMMENT(/^\s*@?rem\b/,/$/,{relevance:10});return{aliases:["bat","cmd"],case_insensitive:!0,illegal:/\/\*/,keywords:{keyword:"if else goto for in do call exit not exist errorlevel defined equ neq lss leq gtr geq",built_in:"prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux shift cd dir echo setlocal endlocal set pause copy append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color comp compact convert date dir diskcomp diskcopy doskey erase fs find findstr format ftype graftabl help keyb label md mkdir mode more move path pause print popd pushd promt rd recover rem rename replace restore rmdir shiftsort start subst time title tree type ver verify vol ping net ipconfig taskkill xcopy ren del"},
 contains:[{className:"variable",begin:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{className:"function",begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",end:"goto:eof",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),c]},{className:"number",begin:"\\b\\d+",relevance:0},c]}});b.registerLanguage("dsconfig",function(a){return{keywords:"dsconfig",contains:[{className:"keyword",begin:"^dsconfig",end:"\\s",excludeEnd:!0,relevance:10},{className:"built_in",begin:"(list|create|get|set|delete)-(\\w+)",
 end:"\\s",excludeEnd:!0,illegal:"!@#$%^&*()",relevance:10},{className:"built_in",begin:"--(\\w+)",end:"\\s",excludeEnd:!0},{className:"string",begin:/"/,end:/"/},{className:"string",begin:/'/,end:/'/},{className:"string",begin:"[\\w-?]+:\\w+",end:"\\W",relevance:0},{className:"string",begin:"\\w+-?\\w+",end:"\\W",relevance:0},a.HASH_COMMENT_MODE]}});b.registerLanguage("dts",function(a){var c={className:"string",variants:[a.inherit(a.QUOTE_STRING_MODE,{begin:'((u8?|U)|L)?"'}),{begin:'(u8?|U)?R"',end:'"',
 contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},b={className:"number",variants:[{begin:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef ifdef ifndef"},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",keywords:{"meta-keyword":"include"},contains:[a.inherit(c,{className:"meta-string"}),{className:"meta-string",begin:"<",end:">",
-illegal:"\\n"}]},c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f={className:"variable",begin:"\\&[a-z\\d_]*\\b"},g={className:"meta-keyword",begin:"/[a-z][a-z\\d-]*/"},l={className:"symbol",begin:"^\\s*[a-zA-Z_][a-zA-Z\\d_]*:"},p={className:"params",begin:"<",end:">",contains:[b,f]},m={className:"class",begin:/[a-zA-Z_][a-zA-Z\d_@]*\s{/,end:/[{;=]/,returnBegin:!0,excludeEnd:!0};return{keywords:"",contains:[{className:"class",begin:"/\\s*{",end:"};",relevance:10,contains:[f,g,l,m,p,a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE,b,c]},f,g,l,m,p,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,c,e,{begin:a.IDENT_RE+"::",keywords:""}]}});b.registerLanguage("dust",function(a){return{aliases:["dst"],case_insensitive:!0,subLanguage:"xml",contains:[{className:"template-tag",begin:/\{[#\/]/,end:/\}/,illegal:/;/,contains:[{className:"name",begin:/[a-zA-Z\.-]+/,starts:{endsWithParent:!0,relevance:0,contains:[a.QUOTE_STRING_MODE]}}]},{className:"template-variable",begin:/\{/,end:/\}/,illegal:/;/,keywords:"if eq ne lt lte gt gte select default math sep"}]}});
-b.registerLanguage("ebnf",function(a){var c=a.COMMENT(/\(\*/,/\*\)/);return{illegal:/\S/,contains:[c,{className:"attribute",begin:/^[ ]*[a-zA-Z][a-zA-Z-]*([\s-]+[a-zA-Z][a-zA-Z]*)*/},{begin:/=/,end:/;/,contains:[c,{className:"meta",begin:/\?.*\?/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]}});b.registerLanguage("elixir",function(a){var c={className:"subst",begin:"#\\{",end:"}",lexemes:"[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?",keywords:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote"},
-b={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]},e={className:"function",beginKeywords:"def defp defmacro",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?",endsParent:!0})]},f=a.inherit(e,{className:"class",beginKeywords:"defimpl defmodule defprotocol defrecord",end:/\bdo\b|$|;/});a=[b,a.HASH_COMMENT_MODE,f,e,{className:"symbol",begin:":(?!\\s)",contains:[b,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],
-relevance:0},{className:"symbol",begin:"[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?:",relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{className:"variable",begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{begin:"->"},{begin:"("+a.RE_STARTERS_RE+")\\s*",contains:[a.HASH_COMMENT_MODE,{className:"regexp",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}],relevance:0}];c.contains=
-a;return{lexemes:"[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?",keywords:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote",contains:a}});b.registerLanguage("elm",function(a){var c={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},b={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},e={begin:"\\(",end:"\\)",illegal:'"',contains:[{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},
-c]};return{keywords:"let in if then else case of where module import exposing type alias as infix infixl infixr port effect command subscription",contains:[{beginKeywords:"port effect module",end:"exposing",keywords:"port effect module where command subscription exposing",contains:[e,c],illegal:"\\W\\.|;"},{begin:"import",end:"$",keywords:"import as exposing",contains:[e,c],illegal:"\\W\\.|;"},{begin:"type",end:"$",keywords:"type alias",contains:[b,e,{begin:"{",end:"}",contains:e.contains},c]},{beginKeywords:"infix infixl infixr",
-end:"$",contains:[a.C_NUMBER_MODE,c]},{begin:"port",end:"$",keywords:"port",contains:[c]},a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,b,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),c,{begin:"->|<-"}],illegal:/;/}});b.registerLanguage("ruby",function(a){var c={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",
+illegal:"\\n"}]},c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f={className:"variable",begin:"\\&[a-z\\d_]*\\b"},g={className:"meta-keyword",begin:"/[a-z][a-z\\d-]*/"},k={className:"symbol",begin:"^\\s*[a-zA-Z_][a-zA-Z\\d_]*:"},m={className:"params",begin:"<",end:">",contains:[b,f]},p={className:"class",begin:/[a-zA-Z_][a-zA-Z\d_@]*\s{/,end:/[{;=]/,returnBegin:!0,excludeEnd:!0};return{keywords:"",contains:[{className:"class",begin:"/\\s*{",end:"};",relevance:10,contains:[f,g,k,p,m,a.C_LINE_COMMENT_MODE,
+a.C_BLOCK_COMMENT_MODE,b,c]},f,g,k,p,m,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,c,e,{begin:a.IDENT_RE+"::",keywords:""}]}});b.registerLanguage("dust",function(a){return{aliases:["dst"],case_insensitive:!0,subLanguage:"xml",contains:[{className:"template-tag",begin:/\{[#\/]/,end:/\}/,illegal:/;/,contains:[{className:"name",begin:/[a-zA-Z\.-]+/,starts:{endsWithParent:!0,relevance:0,contains:[a.QUOTE_STRING_MODE]}}]},{className:"template-variable",begin:/\{/,end:/\}/,illegal:/;/,keywords:"if eq ne lt lte gt gte select default math sep"}]}});
+b.registerLanguage("ebnf",function(a){var c=a.COMMENT(/\(\*/,/\*\)/);return{illegal:/\S/,contains:[c,{className:"attribute",begin:/^[ ]*[a-zA-Z][a-zA-Z-]*([\s-]+[a-zA-Z][a-zA-Z]*)*/},{begin:/=/,end:/;/,contains:[c,{className:"meta",begin:/\?.*\?/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]}});b.registerLanguage("elixir",function(a){var c={className:"subst",begin:"#\\{",end:"}",lexemes:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?",keywords:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote require import with|0"},
+b={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]},e={className:"function",beginKeywords:"def defp defmacro",end:/\B\b/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?",endsParent:!0})]},f=a.inherit(e,{className:"class",beginKeywords:"defimpl defmodule defprotocol defrecord",end:/\bdo\b|$|;/});a=[b,a.HASH_COMMENT_MODE,f,e,{begin:"::"},{className:"symbol",begin:":(?![\\s:])",contains:[b,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],
+relevance:0},{className:"symbol",begin:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?:(?!:)",relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{className:"variable",begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{begin:"->"},{begin:"("+a.RE_STARTERS_RE+")\\s*",contains:[a.HASH_COMMENT_MODE,{className:"regexp",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}],relevance:0}];
+c.contains=a;return{lexemes:"[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?",keywords:"and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote require import with|0",contains:a}});b.registerLanguage("elm",function(a){var c={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},b={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},e={begin:"\\(",end:"\\)",illegal:'"',contains:[{className:"type",
+begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},c]};return{keywords:"let in if then else case of where module import exposing type alias as infix infixl infixr port effect command subscription",contains:[{beginKeywords:"port effect module",end:"exposing",keywords:"port effect module where command subscription exposing",contains:[e,c],illegal:"\\W\\.|;"},{begin:"import",end:"$",keywords:"import as exposing",contains:[e,c],illegal:"\\W\\.|;"},{begin:"type",end:"$",keywords:"type alias",contains:[b,
+e,{begin:"{",end:"}",contains:e.contains},c]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,c]},{begin:"port",end:"$",keywords:"port",contains:[c]},{className:"string",begin:"'\\\\?.",end:"'",illegal:"."},a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,b,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),c,{begin:"->|<-"}],illegal:/;/}});b.registerLanguage("ruby",function(a){var c={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",
 literal:"true false nil"},b={className:"doctag",begin:"@[A-Za-z]+"},e={begin:"#<",end:">"};b=[a.COMMENT("#","$",{contains:[b]}),a.COMMENT("^\\=begin","^\\=end",{contains:[b],relevance:10}),a.COMMENT("^__END__","\\n$")];var f={className:"subst",begin:"#\\{",end:"}",keywords:c},g={className:"string",contains:[a.BACKSLASH_ESCAPE,f],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",
-end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<(-?)\w+$/,end:/^\s*\w+$/}]},l={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:c};a=[g,e,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+
-"::)?"+a.IDENT_RE}]}].concat(b)},{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),l].concat(b)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",
+end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<(-?)\w+$/,end:/^\s*\w+$/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:c};a=[g,e,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+
+"::)?"+a.IDENT_RE}]}].concat(b)},{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(b)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",
 begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:c},{begin:"("+a.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[e,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,f],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(b),relevance:0}].concat(b);
-f.contains=a;l.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:c,illegal:/\/\*/,contains:b.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("erb",function(a){return{subLanguage:"xml",contains:[a.COMMENT("<%#","%>"),{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0}]}});b.registerLanguage("erlang-repl",
+f.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:c,illegal:/\/\*/,contains:b.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("erb",function(a){return{subLanguage:"xml",contains:[a.COMMENT("<%#","%>"),{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0}]}});b.registerLanguage("erlang-repl",
 function(a){return{keywords:{built_in:"spawn spawn_link self",keyword:"after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse|10 query receive rem try when xor"},contains:[{className:"meta",begin:"^[0-9]+> ",relevance:10},a.COMMENT("%","$"),{className:"number",begin:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",relevance:0},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{begin:"\\?(::)?([A-Z]\\w*(::)?)+"},{begin:"->"},{begin:"ok"},{begin:"!"},{begin:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",
 relevance:0},{begin:"[A-Z][a-zA-Z0-9_']*",relevance:0}]}});b.registerLanguage("erlang",function(a){var c={keyword:"after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if let not of orelse|10 query receive rem try when xor",literal:"false true"},b=a.COMMENT("%","$"),e={className:"number",begin:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",relevance:0},f={begin:"fun\\s+[a-z'][a-zA-Z0-9_']*/\\d+"},g={begin:"([a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*|[a-z'][a-zA-Z0-9_']*)\\(",
-end:"\\)",returnBegin:!0,relevance:0,contains:[{begin:"([a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*|[a-z'][a-zA-Z0-9_']*)",relevance:0},{begin:"\\(",end:"\\)",endsWithParent:!0,returnEnd:!0,relevance:0}]},l={begin:"{",end:"}",relevance:0},p={begin:"\\b_([A-Z][A-Za-z0-9_]*)?",relevance:0},m={begin:"[A-Z][a-zA-Z0-9_]*",relevance:0},k={begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0,returnBegin:!0,contains:[{begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0},{begin:"{",end:"}",relevance:0}]},h={beginKeywords:"fun receive if try case",
-end:"end",keywords:c};h.contains=[b,f,a.inherit(a.APOS_STRING_MODE,{className:""}),h,g,a.QUOTE_STRING_MODE,e,l,p,m,k];f=[b,f,h,g,a.QUOTE_STRING_MODE,e,l,p,m,k];g.contains[1].contains=f;l.contains=f;k.contains[1].contains=f;g={className:"params",begin:"\\(",end:"\\)",contains:f};return{aliases:["erl"],keywords:c,illegal:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",contains:[{className:"function",begin:"^[a-z'][a-zA-Z0-9_']*\\s*\\(",end:"->",returnBegin:!0,illegal:"\\(|#|//|/\\*|\\\\|:|;",contains:[g,
-a.inherit(a.TITLE_MODE,{begin:"[a-z'][a-zA-Z0-9_']*"})],starts:{end:";|\\.",keywords:c,contains:f}},b,{begin:"^-",end:"\\.",relevance:0,excludeEnd:!0,returnBegin:!0,lexemes:"-"+a.IDENT_RE,keywords:"-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn -import -include -include_lib -compile -define -else -endif -file -behaviour -behavior -spec",contains:[g]},e,a.QUOTE_STRING_MODE,k,p,m,l,{begin:/\.$/}]}});b.registerLanguage("excel",function(a){return{aliases:["xlsx","xls"],case_insensitive:!0,
+end:"\\)",returnBegin:!0,relevance:0,contains:[{begin:"([a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*|[a-z'][a-zA-Z0-9_']*)",relevance:0},{begin:"\\(",end:"\\)",endsWithParent:!0,returnEnd:!0,relevance:0}]},k={begin:"{",end:"}",relevance:0},m={begin:"\\b_([A-Z][A-Za-z0-9_]*)?",relevance:0},p={begin:"[A-Z][a-zA-Z0-9_]*",relevance:0},n={begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0,returnBegin:!0,contains:[{begin:"#"+a.UNDERSCORE_IDENT_RE,relevance:0},{begin:"{",end:"}",relevance:0}]},h={beginKeywords:"fun receive if try case",
+end:"end",keywords:c};h.contains=[b,f,a.inherit(a.APOS_STRING_MODE,{className:""}),h,g,a.QUOTE_STRING_MODE,e,k,m,p,n];f=[b,f,h,g,a.QUOTE_STRING_MODE,e,k,m,p,n];g.contains[1].contains=f;k.contains=f;n.contains[1].contains=f;g={className:"params",begin:"\\(",end:"\\)",contains:f};return{aliases:["erl"],keywords:c,illegal:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",contains:[{className:"function",begin:"^[a-z'][a-zA-Z0-9_']*\\s*\\(",end:"->",returnBegin:!0,illegal:"\\(|#|//|/\\*|\\\\|:|;",contains:[g,
+a.inherit(a.TITLE_MODE,{begin:"[a-z'][a-zA-Z0-9_']*"})],starts:{end:";|\\.",keywords:c,contains:f}},b,{begin:"^-",end:"\\.",relevance:0,excludeEnd:!0,returnBegin:!0,lexemes:"-"+a.IDENT_RE,keywords:"-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn -import -include -include_lib -compile -define -else -endif -file -behaviour -behavior -spec",contains:[g]},e,a.QUOTE_STRING_MODE,n,m,p,k,{begin:/\.$/}]}});b.registerLanguage("excel",function(a){return{aliases:["xlsx","xls"],case_insensitive:!0,
 lexemes:/[a-zA-Z][\w\.]*/,keywords:{built_in:"ABS ACCRINT ACCRINTM ACOS ACOSH ACOT ACOTH AGGREGATE ADDRESS AMORDEGRC AMORLINC AND ARABIC AREAS ASC ASIN ASINH ATAN ATAN2 ATANH AVEDEV AVERAGE AVERAGEA AVERAGEIF AVERAGEIFS BAHTTEXT BASE BESSELI BESSELJ BESSELK BESSELY BETADIST BETA.DIST BETAINV BETA.INV BIN2DEC BIN2HEX BIN2OCT BINOMDIST BINOM.DIST BINOM.DIST.RANGE BINOM.INV BITAND BITLSHIFT BITOR BITRSHIFT BITXOR CALL CEILING CEILING.MATH CEILING.PRECISE CELL CHAR CHIDIST CHIINV CHITEST CHISQ.DIST CHISQ.DIST.RT CHISQ.INV CHISQ.INV.RT CHISQ.TEST CHOOSE CLEAN CODE COLUMN COLUMNS COMBIN COMBINA COMPLEX CONCAT CONCATENATE CONFIDENCE CONFIDENCE.NORM CONFIDENCE.T CONVERT CORREL COS COSH COT COTH COUNT COUNTA COUNTBLANK COUNTIF COUNTIFS COUPDAYBS COUPDAYS COUPDAYSNC COUPNCD COUPNUM COUPPCD COVAR COVARIANCE.P COVARIANCE.S CRITBINOM CSC CSCH CUBEKPIMEMBER CUBEMEMBER CUBEMEMBERPROPERTY CUBERANKEDMEMBER CUBESET CUBESETCOUNT CUBEVALUE CUMIPMT CUMPRINC DATE DATEDIF DATEVALUE DAVERAGE DAY DAYS DAYS360 DB DBCS DCOUNT DCOUNTA DDB DEC2BIN DEC2HEX DEC2OCT DECIMAL DEGREES DELTA DEVSQ DGET DISC DMAX DMIN DOLLAR DOLLARDE DOLLARFR DPRODUCT DSTDEV DSTDEVP DSUM DURATION DVAR DVARP EDATE EFFECT ENCODEURL EOMONTH ERF ERF.PRECISE ERFC ERFC.PRECISE ERROR.TYPE EUROCONVERT EVEN EXACT EXP EXPON.DIST EXPONDIST FACT FACTDOUBLE FALSE|0 F.DIST FDIST F.DIST.RT FILTERXML FIND FINDB F.INV F.INV.RT FINV FISHER FISHERINV FIXED FLOOR FLOOR.MATH FLOOR.PRECISE FORECAST FORECAST.ETS FORECAST.ETS.CONFINT FORECAST.ETS.SEASONALITY FORECAST.ETS.STAT FORECAST.LINEAR FORMULATEXT FREQUENCY F.TEST FTEST FV FVSCHEDULE GAMMA GAMMA.DIST GAMMADIST GAMMA.INV GAMMAINV GAMMALN GAMMALN.PRECISE GAUSS GCD GEOMEAN GESTEP GETPIVOTDATA GROWTH HARMEAN HEX2BIN HEX2DEC HEX2OCT HLOOKUP HOUR HYPERLINK HYPGEOM.DIST HYPGEOMDIST IF|0 IFERROR IFNA IFS IMABS IMAGINARY IMARGUMENT IMCONJUGATE IMCOS IMCOSH IMCOT IMCSC IMCSCH IMDIV IMEXP IMLN IMLOG10 IMLOG2 IMPOWER IMPRODUCT IMREAL IMSEC IMSECH IMSIN IMSINH IMSQRT IMSUB IMSUM IMTAN INDEX INDIRECT INFO INT INTERCEPT INTRATE IPMT IRR ISBLANK ISERR ISERROR ISEVEN ISFORMULA ISLOGICAL ISNA ISNONTEXT ISNUMBER ISODD ISREF ISTEXT ISO.CEILING ISOWEEKNUM ISPMT JIS KURT LARGE LCM LEFT LEFTB LEN LENB LINEST LN LOG LOG10 LOGEST LOGINV LOGNORM.DIST LOGNORMDIST LOGNORM.INV LOOKUP LOWER MATCH MAX MAXA MAXIFS MDETERM MDURATION MEDIAN MID MIDBs MIN MINIFS MINA MINUTE MINVERSE MIRR MMULT MOD MODE MODE.MULT MODE.SNGL MONTH MROUND MULTINOMIAL MUNIT N NA NEGBINOM.DIST NEGBINOMDIST NETWORKDAYS NETWORKDAYS.INTL NOMINAL NORM.DIST NORMDIST NORMINV NORM.INV NORM.S.DIST NORMSDIST NORM.S.INV NORMSINV NOT NOW NPER NPV NUMBERVALUE OCT2BIN OCT2DEC OCT2HEX ODD ODDFPRICE ODDFYIELD ODDLPRICE ODDLYIELD OFFSET OR PDURATION PEARSON PERCENTILE.EXC PERCENTILE.INC PERCENTILE PERCENTRANK.EXC PERCENTRANK.INC PERCENTRANK PERMUT PERMUTATIONA PHI PHONETIC PI PMT POISSON.DIST POISSON POWER PPMT PRICE PRICEDISC PRICEMAT PROB PRODUCT PROPER PV QUARTILE QUARTILE.EXC QUARTILE.INC QUOTIENT RADIANS RAND RANDBETWEEN RANK.AVG RANK.EQ RANK RATE RECEIVED REGISTER.ID REPLACE REPLACEB REPT RIGHT RIGHTB ROMAN ROUND ROUNDDOWN ROUNDUP ROW ROWS RRI RSQ RTD SEARCH SEARCHB SEC SECH SECOND SERIESSUM SHEET SHEETS SIGN SIN SINH SKEW SKEW.P SLN SLOPE SMALL SQL.REQUEST SQRT SQRTPI STANDARDIZE STDEV STDEV.P STDEV.S STDEVA STDEVP STDEVPA STEYX SUBSTITUTE SUBTOTAL SUM SUMIF SUMIFS SUMPRODUCT SUMSQ SUMX2MY2 SUMX2PY2 SUMXMY2 SWITCH SYD T TAN TANH TBILLEQ TBILLPRICE TBILLYIELD T.DIST T.DIST.2T T.DIST.RT TDIST TEXT TEXTJOIN TIME TIMEVALUE T.INV T.INV.2T TINV TODAY TRANSPOSE TREND TRIM TRIMMEAN TRUE|0 TRUNC T.TEST TTEST TYPE UNICHAR UNICODE UPPER VALUE VAR VAR.P VAR.S VARA VARP VARPA VDB VLOOKUP WEBSERVICE WEEKDAY WEEKNUM WEIBULL WEIBULL.DIST WORKDAY WORKDAY.INTL XIRR XNPV XOR YEAR YEARFRAC YIELD YIELDDISC YIELDMAT Z.TEST ZTEST"},
 contains:[{begin:/^=/,end:/[^=]/,returnEnd:!0,illegal:/=/,relevance:10},{className:"symbol",begin:/\b[A-Z]{1,2}\d+\b/,end:/[^\d]/,excludeEnd:!0,relevance:0},{className:"symbol",begin:/[A-Z]{0,2}\d*:[A-Z]{0,2}\d*/,relevance:0},a.BACKSLASH_ESCAPE,a.QUOTE_STRING_MODE,{className:"number",begin:a.NUMBER_RE+"(%)?",relevance:0},a.COMMENT(/\bN\(/,/\)/,{excludeBegin:!0,excludeEnd:!0,illegal:/\n/})]}});b.registerLanguage("fix",function(a){return{contains:[{begin:/[^\u2401\u0001]+/,end:/[\u2401\u0001]/,excludeEnd:!0,
 returnBegin:!0,returnEnd:!1,contains:[{begin:/([^\u2401\u0001=]+)/,end:/=([^\u2401\u0001=]+)/,returnEnd:!0,returnBegin:!1,className:"attr"},{begin:/=/,end:/([\u2401\u0001])/,excludeEnd:!0,excludeBegin:!0,className:"string"}]}],case_insensitive:!0}});b.registerLanguage("flix",function(a){return{keywords:{literal:"true false",keyword:"case class def else enum if impl import in lat rel index let match namespace switch type yield with"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",
@@ -167,21 +176,25 @@
 b={className:"symbol",variants:[{begin:/=[lgenxc]=/},{begin:/\$/}]},e={className:"comment",variants:[{begin:"'",end:"'"},{begin:'"',end:'"'}],illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},f={begin:"/",end:"/",keywords:c,contains:[e,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_NUMBER_MODE]};e={begin:/[a-z][a-z0-9_]*(\([a-z0-9_, ]*\))?[ \t]+/,excludeBegin:!0,end:"$",endsWithParent:!0,contains:[e,f,{className:"comment",begin:/([ ]*[a-z0-9&#*=?@>\\<:\-,()$\[\]_.{}!+%^]+)+/,
 relevance:0}]};return{aliases:["gms"],case_insensitive:!0,keywords:c,contains:[a.COMMENT(/^\$ontext/,/^\$offtext/),{className:"meta",begin:"^\\$[a-z0-9]+",end:"$",returnBegin:!0,contains:[{className:"meta-keyword",begin:"^\\$[a-z0-9]+"}]},a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{beginKeywords:"set sets parameter parameters variable variables scalar scalars equation equations",end:";",contains:[a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,
 a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,f,e]},{beginKeywords:"table",end:";",returnBegin:!0,contains:[{beginKeywords:"table",end:"$",contains:[e]},a.COMMENT("^\\*","$"),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_NUMBER_MODE]},{className:"function",begin:/^[a-z][a-z0-9_,\-+' ()$]+\.{2}/,returnBegin:!0,contains:[{className:"title",begin:/^[a-z0-9_]+/},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0},b]},a.C_NUMBER_MODE,
-b]}});b.registerLanguage("gauss",function(a){var c={keyword:"and bool break call callexe checkinterrupt clear clearg closeall cls comlog compile continue create debug declare delete disable dlibrary dllcall do dos ed edit else elseif enable end endfor endif endp endo errorlog errorlogat expr external fn for format goto gosub graph if keyword let lib library line load loadarray loadexe loadf loadk loadm loadp loads loadx local locate loopnextindex lprint lpwidth lshow matrix msym ndpclex new not open or output outwidth plot plotsym pop prcsn print printdos proc push retp return rndcon rndmod rndmult rndseed run save saveall screen scroll setarray show sparse stop string struct system trace trap threadfor threadendfor threadbegin threadjoin threadstat threadend until use while winprint",
-built_in:"abs acf aconcat aeye amax amean AmericanBinomCall AmericanBinomCall_Greeks AmericanBinomCall_ImpVol AmericanBinomPut AmericanBinomPut_Greeks AmericanBinomPut_ImpVol AmericanBSCall AmericanBSCall_Greeks AmericanBSCall_ImpVol AmericanBSPut AmericanBSPut_Greeks AmericanBSPut_ImpVol amin amult annotationGetDefaults annotationSetBkd annotationSetFont annotationSetLineColor annotationSetLineStyle annotationSetLineThickness annualTradingDays arccos arcsin areshape arrayalloc arrayindex arrayinit arraytomat asciiload asclabel astd astds asum atan atan2 atranspose axmargin balance band bandchol bandcholsol bandltsol bandrv bandsolpd bar base10 begwind besselj bessely beta box boxcox cdfBeta cdfBetaInv cdfBinomial cdfBinomialInv cdfBvn cdfBvn2 cdfBvn2e cdfCauchy cdfCauchyInv cdfChic cdfChii cdfChinc cdfChincInv cdfExp cdfExpInv cdfFc cdfFnc cdfFncInv cdfGam cdfGenPareto cdfHyperGeo cdfLaplace cdfLaplaceInv cdfLogistic cdfLogisticInv cdfmControlCreate cdfMvn cdfMvn2e cdfMvnce cdfMvne cdfMvt2e cdfMvtce cdfMvte cdfN cdfN2 cdfNc cdfNegBinomial cdfNegBinomialInv cdfNi cdfPoisson cdfPoissonInv cdfRayleigh cdfRayleighInv cdfTc cdfTci cdfTnc cdfTvn cdfWeibull cdfWeibullInv cdir ceil ChangeDir chdir chiBarSquare chol choldn cholsol cholup chrs close code cols colsf combinate combinated complex con cond conj cons ConScore contour conv convertsatostr convertstrtosa corrm corrms corrvc corrx corrxs cos cosh counts countwts crossprd crout croutp csrcol csrlin csvReadM csvReadSA cumprodc cumsumc curve cvtos datacreate datacreatecomplex datalist dataload dataloop dataopen datasave date datestr datestring datestrymd dayinyr dayofweek dbAddDatabase dbClose dbCommit dbCreateQuery dbExecQuery dbGetConnectOptions dbGetDatabaseName dbGetDriverName dbGetDrivers dbGetHostName dbGetLastErrorNum dbGetLastErrorText dbGetNumericalPrecPolicy dbGetPassword dbGetPort dbGetTableHeaders dbGetTables dbGetUserName dbHasFeature dbIsDriverAvailable dbIsOpen dbIsOpenError dbOpen dbQueryBindValue dbQueryClear dbQueryCols dbQueryExecPrepared dbQueryFetchAllM dbQueryFetchAllSA dbQueryFetchOneM dbQueryFetchOneSA dbQueryFinish dbQueryGetBoundValue dbQueryGetBoundValues dbQueryGetField dbQueryGetLastErrorNum dbQueryGetLastErrorText dbQueryGetLastInsertID dbQueryGetLastQuery dbQueryGetPosition dbQueryIsActive dbQueryIsForwardOnly dbQueryIsNull dbQueryIsSelect dbQueryIsValid dbQueryPrepare dbQueryRows dbQuerySeek dbQuerySeekFirst dbQuerySeekLast dbQuerySeekNext dbQuerySeekPrevious dbQuerySetForwardOnly dbRemoveDatabase dbRollback dbSetConnectOptions dbSetDatabaseName dbSetHostName dbSetNumericalPrecPolicy dbSetPort dbSetUserName dbTransaction DeleteFile delif delrows denseToSp denseToSpRE denToZero design det detl dfft dffti diag diagrv digamma doswin DOSWinCloseall DOSWinOpen dotfeq dotfeqmt dotfge dotfgemt dotfgt dotfgtmt dotfle dotflemt dotflt dotfltmt dotfne dotfnemt draw drop dsCreate dstat dstatmt dstatmtControlCreate dtdate dtday dttime dttodtv dttostr dttoutc dtvnormal dtvtodt dtvtoutc dummy dummybr dummydn eig eigh eighv eigv elapsedTradingDays endwind envget eof eqSolve eqSolvemt eqSolvemtControlCreate eqSolvemtOutCreate eqSolveset erf erfc erfccplx erfcplx error etdays ethsec etstr EuropeanBinomCall EuropeanBinomCall_Greeks EuropeanBinomCall_ImpVol EuropeanBinomPut EuropeanBinomPut_Greeks EuropeanBinomPut_ImpVol EuropeanBSCall EuropeanBSCall_Greeks EuropeanBSCall_ImpVol EuropeanBSPut EuropeanBSPut_Greeks EuropeanBSPut_ImpVol exctsmpl exec execbg exp extern eye fcheckerr fclearerr feq feqmt fflush fft ffti fftm fftmi fftn fge fgemt fgets fgetsa fgetsat fgetst fgt fgtmt fileinfo filesa fle flemt floor flt fltmt fmod fne fnemt fonts fopen formatcv formatnv fputs fputst fseek fstrerror ftell ftocv ftos ftostrC gamma gammacplx gammaii gausset gdaAppend gdaCreate gdaDStat gdaDStatMat gdaGetIndex gdaGetName gdaGetNames gdaGetOrders gdaGetType gdaGetTypes gdaGetVarInfo gdaIsCplx gdaLoad gdaPack gdaRead gdaReadByIndex gdaReadSome gdaReadSparse gdaReadStruct gdaReportVarInfo gdaSave gdaUpdate gdaUpdateAndPack gdaVars gdaWrite gdaWrite32 gdaWriteSome getarray getdims getf getGAUSShome getmatrix getmatrix4D getname getnamef getNextTradingDay getNextWeekDay getnr getorders getpath getPreviousTradingDay getPreviousWeekDay getRow getscalar3D getscalar4D getTrRow getwind glm gradcplx gradMT gradMTm gradMTT gradMTTm gradp graphprt graphset hasimag header headermt hess hessMT hessMTg hessMTgw hessMTm hessMTmw hessMTT hessMTTg hessMTTgw hessMTTm hessMTw hessp hist histf histp hsec imag indcv indexcat indices indices2 indicesf indicesfn indnv indsav integrate1d integrateControlCreate intgrat2 intgrat3 inthp1 inthp2 inthp3 inthp4 inthpControlCreate intquad1 intquad2 intquad3 intrleav intrleavsa intrsect intsimp inv invpd invswp iscplx iscplxf isden isinfnanmiss ismiss key keyav keyw lag lag1 lagn lapEighb lapEighi lapEighvb lapEighvi lapgEig lapgEigh lapgEighv lapgEigv lapgSchur lapgSvdcst lapgSvds lapgSvdst lapSvdcusv lapSvds lapSvdusv ldlp ldlsol linSolve listwise ln lncdfbvn lncdfbvn2 lncdfmvn lncdfn lncdfn2 lncdfnc lnfact lngammacplx lnpdfmvn lnpdfmvt lnpdfn lnpdft loadd loadstruct loadwind loess loessmt loessmtControlCreate log loglog logx logy lower lowmat lowmat1 ltrisol lu lusol machEpsilon make makevars makewind margin matalloc matinit mattoarray maxbytes maxc maxindc maxv maxvec mbesselei mbesselei0 mbesselei1 mbesseli mbesseli0 mbesseli1 meanc median mergeby mergevar minc minindc minv miss missex missrv moment momentd movingave movingaveExpwgt movingaveWgt nextindex nextn nextnevn nextwind ntos null null1 numCombinations ols olsmt olsmtControlCreate olsqr olsqr2 olsqrmt ones optn optnevn orth outtyp pacf packedToSp packr parse pause pdfCauchy pdfChi pdfExp pdfGenPareto pdfHyperGeo pdfLaplace pdfLogistic pdfn pdfPoisson pdfRayleigh pdfWeibull pi pinv pinvmt plotAddArrow plotAddBar plotAddBox plotAddHist plotAddHistF plotAddHistP plotAddPolar plotAddScatter plotAddShape plotAddTextbox plotAddTS plotAddXY plotArea plotBar plotBox plotClearLayout plotContour plotCustomLayout plotGetDefaults plotHist plotHistF plotHistP plotLayout plotLogLog plotLogX plotLogY plotOpenWindow plotPolar plotSave plotScatter plotSetAxesPen plotSetBar plotSetBarFill plotSetBarStacked plotSetBkdColor plotSetFill plotSetGrid plotSetLegend plotSetLineColor plotSetLineStyle plotSetLineSymbol plotSetLineThickness plotSetNewWindow plotSetTitle plotSetWhichYAxis plotSetXAxisShow plotSetXLabel plotSetXRange plotSetXTicInterval plotSetXTicLabel plotSetYAxisShow plotSetYLabel plotSetYRange plotSetZAxisShow plotSetZLabel plotSurface plotTS plotXY polar polychar polyeval polygamma polyint polymake polymat polymroot polymult polyroot pqgwin previousindex princomp printfm printfmt prodc psi putarray putf putvals pvCreate pvGetIndex pvGetParNames pvGetParVector pvLength pvList pvPack pvPacki pvPackm pvPackmi pvPacks pvPacksi pvPacksm pvPacksmi pvPutParVector pvTest pvUnpack QNewton QNewtonmt QNewtonmtControlCreate QNewtonmtOutCreate QNewtonSet QProg QProgmt QProgmtInCreate qqr qqre qqrep qr qre qrep qrsol qrtsol qtyr qtyre qtyrep quantile quantiled qyr qyre qyrep qz rank rankindx readr real reclassify reclassifyCuts recode recserar recsercp recserrc rerun rescale reshape rets rev rfft rffti rfftip rfftn rfftnp rfftp rndBernoulli rndBeta rndBinomial rndCauchy rndChiSquare rndCon rndCreateState rndExp rndGamma rndGeo rndGumbel rndHyperGeo rndi rndKMbeta rndKMgam rndKMi rndKMn rndKMnb rndKMp rndKMu rndKMvm rndLaplace rndLCbeta rndLCgam rndLCi rndLCn rndLCnb rndLCp rndLCu rndLCvm rndLogNorm rndMTu rndMVn rndMVt rndn rndnb rndNegBinomial rndp rndPoisson rndRayleigh rndStateSkip rndu rndvm rndWeibull rndWishart rotater round rows rowsf rref sampleData satostrC saved saveStruct savewind scale scale3d scalerr scalinfnanmiss scalmiss schtoc schur searchsourcepath seekr select selif seqa seqm setdif setdifsa setvars setvwrmode setwind shell shiftr sin singleindex sinh sleep solpd sortc sortcc sortd sorthc sorthcc sortind sortindc sortmc sortr sortrc spBiconjGradSol spChol spConjGradSol spCreate spDenseSubmat spDiagRvMat spEigv spEye spLDL spline spLU spNumNZE spOnes spreadSheetReadM spreadSheetReadSA spreadSheetWrite spScale spSubmat spToDense spTrTDense spTScalar spZeros sqpSolve sqpSolveMT sqpSolveMTControlCreate sqpSolveMTlagrangeCreate sqpSolveMToutCreate sqpSolveSet sqrt statements stdc stdsc stocv stof strcombine strindx strlen strput strrindx strsect strsplit strsplitPad strtodt strtof strtofcplx strtriml strtrimr strtrunc strtruncl strtruncpad strtruncr submat subscat substute subvec sumc sumr surface svd svd1 svd2 svdcusv svds svdusv sysstate tab tan tanh tempname threadBegin threadEnd threadEndFor threadFor threadJoin threadStat time timedt timestr timeutc title tkf2eps tkf2ps tocart todaydt toeplitz token topolar trapchk trigamma trimr trunc type typecv typef union unionsa uniqindx uniqindxsa unique uniquesa upmat upmat1 upper utctodt utctodtv utrisol vals varCovMS varCovXS varget vargetl varmall varmares varput varputl vartypef vcm vcms vcx vcxs vec vech vecr vector vget view viewxyz vlist vnamecv volume vput vread vtypecv wait waitc walkindex where window writer xlabel xlsGetSheetCount xlsGetSheetSize xlsGetSheetTypes xlsMakeRange xlsReadM xlsReadSA xlsWrite xlsWriteM xlsWriteSA xpnd xtics xy xyz ylabel ytics zeros zeta zlabel ztics cdfEmpirical dot h5create h5open h5read h5readAttribute h5write h5writeAttribute ldl plotAddErrorBar plotAddSurface plotCDFEmpirical plotSetColormap plotSetContourLabels plotSetLegendFont plotSetTextInterpreter plotSetXTicCount plotSetYTicCount plotSetZLevels powerm strjoin strtrim sylvester",
-literal:"DB_AFTER_LAST_ROW DB_ALL_TABLES DB_BATCH_OPERATIONS DB_BEFORE_FIRST_ROW DB_BLOB DB_EVENT_NOTIFICATIONS DB_FINISH_QUERY DB_HIGH_PRECISION DB_LAST_INSERT_ID DB_LOW_PRECISION_DOUBLE DB_LOW_PRECISION_INT32 DB_LOW_PRECISION_INT64 DB_LOW_PRECISION_NUMBERS DB_MULTIPLE_RESULT_SETS DB_NAMED_PLACEHOLDERS DB_POSITIONAL_PLACEHOLDERS DB_PREPARED_QUERIES DB_QUERY_SIZE DB_SIMPLE_LOCKING DB_SYSTEM_TABLES DB_TABLES DB_TRANSACTIONS DB_UNICODE DB_VIEWS"},b={className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"define definecs|10 undef ifdef ifndef iflight ifdllcall ifmac ifos2win ifunix else endif lineson linesoff srcfile srcline"},
-contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",keywords:{"meta-keyword":"include"},contains:[{className:"meta-string",begin:'"',end:'"',illegal:"\\n"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},e=a.UNDERSCORE_IDENT_RE+"\\s*\\(?",f=[{className:"params",begin:/\(/,end:/\)/,keywords:c,relevance:0,contains:[a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}];return{aliases:["gss"],case_insensitive:!0,keywords:c,illegal:"(\\{[%#]|[%#]\\})",contains:[a.C_NUMBER_MODE,
-a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT("@","@"),b,{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{className:"function",beginKeywords:"proc keyword",end:";",excludeEnd:!0,keywords:c,contains:[{begin:e,returnBegin:!0,contains:[a.UNDERSCORE_TITLE_MODE],relevance:0},a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b].concat(f)},{className:"function",beginKeywords:"fn",end:";",excludeEnd:!0,keywords:c,contains:[{begin:e+a.IDENT_RE+"\\)?\\s*\\=\\s*",returnBegin:!0,
-contains:[a.UNDERSCORE_TITLE_MODE],relevance:0},a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE].concat(f)},{className:"function",begin:"\\bexternal (proc|keyword|fn)\\s+",end:";",excludeEnd:!0,keywords:c,contains:[{begin:e,returnBegin:!0,contains:[a.UNDERSCORE_TITLE_MODE],relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"function",begin:"\\bexternal (matrix|string|array|sparse matrix|struct "+a.IDENT_RE+")\\s+",end:";",excludeEnd:!0,keywords:c,contains:[a.C_LINE_COMMENT_MODE,
-a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("gcode",function(a){a=[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT(/\(/,/\)/),a.inherit(a.C_NUMBER_MODE,{begin:"([-+]?([0-9]*\\.?[0-9]+\\.?))|"+a.C_NUMBER_RE}),a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"name",begin:"([G])([0-9]+\\.?[0-9]?)"},{className:"name",begin:"([M])([0-9]+\\.?[0-9]?)"},{className:"attr",begin:"(VC|VS|#)",end:"(\\d+)"},{className:"attr",begin:"(VZOFX|VZOFY|VZOFZ)"},
-{className:"built_in",begin:"(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)",end:"([-+]?([0-9]*\\.?[0-9]+\\.?))(\\])"},{className:"symbol",variants:[{begin:"N",end:"\\d+",illegal:"\\W"}]}];return{aliases:["nc"],case_insensitive:!0,lexemes:"[A-Z_][A-Z0-9_.]*",keywords:"IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT EQ LT GT NE GE LE OR XOR",contains:[{className:"meta",begin:"\\%"},{className:"meta",begin:"([O])([0-9]+)"}].concat(a)}});b.registerLanguage("gherkin",function(a){return{aliases:["feature"],
-keywords:"Feature Background Ability Business Need Scenario Scenarios Scenario Outline Scenario Template Examples Given And Then But When",contains:[{className:"symbol",begin:"\\*",relevance:0},{className:"meta",begin:"@[^@\\s]+"},{begin:"\\|",end:"\\|\\w*$",contains:[{className:"string",begin:"[^|]+"}]},{className:"variable",begin:"<",end:">"},a.HASH_COMMENT_MODE,{className:"string",begin:'"""',end:'"""'},a.QUOTE_STRING_MODE]}});b.registerLanguage("glsl",function(a){return{keywords:{keyword:"break continue discard do else for if return while switch case default attribute binding buffer ccw centroid centroid varying coherent column_major const cw depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip triangles triangles_adjacency uniform varying vertices volatile writeonly",
+b]}});b.registerLanguage("gauss",function(a){var c={keyword:"bool break call callexe checkinterrupt clear clearg closeall cls comlog compile continue create debug declare delete disable dlibrary dllcall do dos ed edit else elseif enable end endfor endif endp endo errorlog errorlogat expr external fn for format goto gosub graph if keyword let lib library line load loadarray loadexe loadf loadk loadm loadp loads loadx local locate loopnextindex lprint lpwidth lshow matrix msym ndpclex new open output outwidth plot plotsym pop prcsn print printdos proc push retp return rndcon rndmod rndmult rndseed run save saveall screen scroll setarray show sparse stop string struct system trace trap threadfor threadendfor threadbegin threadjoin threadstat threadend until use while winprint ne ge le gt lt and xor or not eq eqv",
+built_in:"abs acf aconcat aeye amax amean AmericanBinomCall AmericanBinomCall_Greeks AmericanBinomCall_ImpVol AmericanBinomPut AmericanBinomPut_Greeks AmericanBinomPut_ImpVol AmericanBSCall AmericanBSCall_Greeks AmericanBSCall_ImpVol AmericanBSPut AmericanBSPut_Greeks AmericanBSPut_ImpVol amin amult annotationGetDefaults annotationSetBkd annotationSetFont annotationSetLineColor annotationSetLineStyle annotationSetLineThickness annualTradingDays arccos arcsin areshape arrayalloc arrayindex arrayinit arraytomat asciiload asclabel astd astds asum atan atan2 atranspose axmargin balance band bandchol bandcholsol bandltsol bandrv bandsolpd bar base10 begwind besselj bessely beta box boxcox cdfBeta cdfBetaInv cdfBinomial cdfBinomialInv cdfBvn cdfBvn2 cdfBvn2e cdfCauchy cdfCauchyInv cdfChic cdfChii cdfChinc cdfChincInv cdfExp cdfExpInv cdfFc cdfFnc cdfFncInv cdfGam cdfGenPareto cdfHyperGeo cdfLaplace cdfLaplaceInv cdfLogistic cdfLogisticInv cdfmControlCreate cdfMvn cdfMvn2e cdfMvnce cdfMvne cdfMvt2e cdfMvtce cdfMvte cdfN cdfN2 cdfNc cdfNegBinomial cdfNegBinomialInv cdfNi cdfPoisson cdfPoissonInv cdfRayleigh cdfRayleighInv cdfTc cdfTci cdfTnc cdfTvn cdfWeibull cdfWeibullInv cdir ceil ChangeDir chdir chiBarSquare chol choldn cholsol cholup chrs close code cols colsf combinate combinated complex con cond conj cons ConScore contour conv convertsatostr convertstrtosa corrm corrms corrvc corrx corrxs cos cosh counts countwts crossprd crout croutp csrcol csrlin csvReadM csvReadSA cumprodc cumsumc curve cvtos datacreate datacreatecomplex datalist dataload dataloop dataopen datasave date datestr datestring datestrymd dayinyr dayofweek dbAddDatabase dbClose dbCommit dbCreateQuery dbExecQuery dbGetConnectOptions dbGetDatabaseName dbGetDriverName dbGetDrivers dbGetHostName dbGetLastErrorNum dbGetLastErrorText dbGetNumericalPrecPolicy dbGetPassword dbGetPort dbGetTableHeaders dbGetTables dbGetUserName dbHasFeature dbIsDriverAvailable dbIsOpen dbIsOpenError dbOpen dbQueryBindValue dbQueryClear dbQueryCols dbQueryExecPrepared dbQueryFetchAllM dbQueryFetchAllSA dbQueryFetchOneM dbQueryFetchOneSA dbQueryFinish dbQueryGetBoundValue dbQueryGetBoundValues dbQueryGetField dbQueryGetLastErrorNum dbQueryGetLastErrorText dbQueryGetLastInsertID dbQueryGetLastQuery dbQueryGetPosition dbQueryIsActive dbQueryIsForwardOnly dbQueryIsNull dbQueryIsSelect dbQueryIsValid dbQueryPrepare dbQueryRows dbQuerySeek dbQuerySeekFirst dbQuerySeekLast dbQuerySeekNext dbQuerySeekPrevious dbQuerySetForwardOnly dbRemoveDatabase dbRollback dbSetConnectOptions dbSetDatabaseName dbSetHostName dbSetNumericalPrecPolicy dbSetPort dbSetUserName dbTransaction DeleteFile delif delrows denseToSp denseToSpRE denToZero design det detl dfft dffti diag diagrv digamma doswin DOSWinCloseall DOSWinOpen dotfeq dotfeqmt dotfge dotfgemt dotfgt dotfgtmt dotfle dotflemt dotflt dotfltmt dotfne dotfnemt draw drop dsCreate dstat dstatmt dstatmtControlCreate dtdate dtday dttime dttodtv dttostr dttoutc dtvnormal dtvtodt dtvtoutc dummy dummybr dummydn eig eigh eighv eigv elapsedTradingDays endwind envget eof eqSolve eqSolvemt eqSolvemtControlCreate eqSolvemtOutCreate eqSolveset erf erfc erfccplx erfcplx error etdays ethsec etstr EuropeanBinomCall EuropeanBinomCall_Greeks EuropeanBinomCall_ImpVol EuropeanBinomPut EuropeanBinomPut_Greeks EuropeanBinomPut_ImpVol EuropeanBSCall EuropeanBSCall_Greeks EuropeanBSCall_ImpVol EuropeanBSPut EuropeanBSPut_Greeks EuropeanBSPut_ImpVol exctsmpl exec execbg exp extern eye fcheckerr fclearerr feq feqmt fflush fft ffti fftm fftmi fftn fge fgemt fgets fgetsa fgetsat fgetst fgt fgtmt fileinfo filesa fle flemt floor flt fltmt fmod fne fnemt fonts fopen formatcv formatnv fputs fputst fseek fstrerror ftell ftocv ftos ftostrC gamma gammacplx gammaii gausset gdaAppend gdaCreate gdaDStat gdaDStatMat gdaGetIndex gdaGetName gdaGetNames gdaGetOrders gdaGetType gdaGetTypes gdaGetVarInfo gdaIsCplx gdaLoad gdaPack gdaRead gdaReadByIndex gdaReadSome gdaReadSparse gdaReadStruct gdaReportVarInfo gdaSave gdaUpdate gdaUpdateAndPack gdaVars gdaWrite gdaWrite32 gdaWriteSome getarray getdims getf getGAUSShome getmatrix getmatrix4D getname getnamef getNextTradingDay getNextWeekDay getnr getorders getpath getPreviousTradingDay getPreviousWeekDay getRow getscalar3D getscalar4D getTrRow getwind glm gradcplx gradMT gradMTm gradMTT gradMTTm gradp graphprt graphset hasimag header headermt hess hessMT hessMTg hessMTgw hessMTm hessMTmw hessMTT hessMTTg hessMTTgw hessMTTm hessMTw hessp hist histf histp hsec imag indcv indexcat indices indices2 indicesf indicesfn indnv indsav integrate1d integrateControlCreate intgrat2 intgrat3 inthp1 inthp2 inthp3 inthp4 inthpControlCreate intquad1 intquad2 intquad3 intrleav intrleavsa intrsect intsimp inv invpd invswp iscplx iscplxf isden isinfnanmiss ismiss key keyav keyw lag lag1 lagn lapEighb lapEighi lapEighvb lapEighvi lapgEig lapgEigh lapgEighv lapgEigv lapgSchur lapgSvdcst lapgSvds lapgSvdst lapSvdcusv lapSvds lapSvdusv ldlp ldlsol linSolve listwise ln lncdfbvn lncdfbvn2 lncdfmvn lncdfn lncdfn2 lncdfnc lnfact lngammacplx lnpdfmvn lnpdfmvt lnpdfn lnpdft loadd loadstruct loadwind loess loessmt loessmtControlCreate log loglog logx logy lower lowmat lowmat1 ltrisol lu lusol machEpsilon make makevars makewind margin matalloc matinit mattoarray maxbytes maxc maxindc maxv maxvec mbesselei mbesselei0 mbesselei1 mbesseli mbesseli0 mbesseli1 meanc median mergeby mergevar minc minindc minv miss missex missrv moment momentd movingave movingaveExpwgt movingaveWgt nextindex nextn nextnevn nextwind ntos null null1 numCombinations ols olsmt olsmtControlCreate olsqr olsqr2 olsqrmt ones optn optnevn orth outtyp pacf packedToSp packr parse pause pdfCauchy pdfChi pdfExp pdfGenPareto pdfHyperGeo pdfLaplace pdfLogistic pdfn pdfPoisson pdfRayleigh pdfWeibull pi pinv pinvmt plotAddArrow plotAddBar plotAddBox plotAddHist plotAddHistF plotAddHistP plotAddPolar plotAddScatter plotAddShape plotAddTextbox plotAddTS plotAddXY plotArea plotBar plotBox plotClearLayout plotContour plotCustomLayout plotGetDefaults plotHist plotHistF plotHistP plotLayout plotLogLog plotLogX plotLogY plotOpenWindow plotPolar plotSave plotScatter plotSetAxesPen plotSetBar plotSetBarFill plotSetBarStacked plotSetBkdColor plotSetFill plotSetGrid plotSetLegend plotSetLineColor plotSetLineStyle plotSetLineSymbol plotSetLineThickness plotSetNewWindow plotSetTitle plotSetWhichYAxis plotSetXAxisShow plotSetXLabel plotSetXRange plotSetXTicInterval plotSetXTicLabel plotSetYAxisShow plotSetYLabel plotSetYRange plotSetZAxisShow plotSetZLabel plotSurface plotTS plotXY polar polychar polyeval polygamma polyint polymake polymat polymroot polymult polyroot pqgwin previousindex princomp printfm printfmt prodc psi putarray putf putvals pvCreate pvGetIndex pvGetParNames pvGetParVector pvLength pvList pvPack pvPacki pvPackm pvPackmi pvPacks pvPacksi pvPacksm pvPacksmi pvPutParVector pvTest pvUnpack QNewton QNewtonmt QNewtonmtControlCreate QNewtonmtOutCreate QNewtonSet QProg QProgmt QProgmtInCreate qqr qqre qqrep qr qre qrep qrsol qrtsol qtyr qtyre qtyrep quantile quantiled qyr qyre qyrep qz rank rankindx readr real reclassify reclassifyCuts recode recserar recsercp recserrc rerun rescale reshape rets rev rfft rffti rfftip rfftn rfftnp rfftp rndBernoulli rndBeta rndBinomial rndCauchy rndChiSquare rndCon rndCreateState rndExp rndGamma rndGeo rndGumbel rndHyperGeo rndi rndKMbeta rndKMgam rndKMi rndKMn rndKMnb rndKMp rndKMu rndKMvm rndLaplace rndLCbeta rndLCgam rndLCi rndLCn rndLCnb rndLCp rndLCu rndLCvm rndLogNorm rndMTu rndMVn rndMVt rndn rndnb rndNegBinomial rndp rndPoisson rndRayleigh rndStateSkip rndu rndvm rndWeibull rndWishart rotater round rows rowsf rref sampleData satostrC saved saveStruct savewind scale scale3d scalerr scalinfnanmiss scalmiss schtoc schur searchsourcepath seekr select selif seqa seqm setdif setdifsa setvars setvwrmode setwind shell shiftr sin singleindex sinh sleep solpd sortc sortcc sortd sorthc sorthcc sortind sortindc sortmc sortr sortrc spBiconjGradSol spChol spConjGradSol spCreate spDenseSubmat spDiagRvMat spEigv spEye spLDL spline spLU spNumNZE spOnes spreadSheetReadM spreadSheetReadSA spreadSheetWrite spScale spSubmat spToDense spTrTDense spTScalar spZeros sqpSolve sqpSolveMT sqpSolveMTControlCreate sqpSolveMTlagrangeCreate sqpSolveMToutCreate sqpSolveSet sqrt statements stdc stdsc stocv stof strcombine strindx strlen strput strrindx strsect strsplit strsplitPad strtodt strtof strtofcplx strtriml strtrimr strtrunc strtruncl strtruncpad strtruncr submat subscat substute subvec sumc sumr surface svd svd1 svd2 svdcusv svds svdusv sysstate tab tan tanh tempname time timedt timestr timeutc title tkf2eps tkf2ps tocart todaydt toeplitz token topolar trapchk trigamma trimr trunc type typecv typef union unionsa uniqindx uniqindxsa unique uniquesa upmat upmat1 upper utctodt utctodtv utrisol vals varCovMS varCovXS varget vargetl varmall varmares varput varputl vartypef vcm vcms vcx vcxs vec vech vecr vector vget view viewxyz vlist vnamecv volume vput vread vtypecv wait waitc walkindex where window writer xlabel xlsGetSheetCount xlsGetSheetSize xlsGetSheetTypes xlsMakeRange xlsReadM xlsReadSA xlsWrite xlsWriteM xlsWriteSA xpnd xtics xy xyz ylabel ytics zeros zeta zlabel ztics cdfEmpirical dot h5create h5open h5read h5readAttribute h5write h5writeAttribute ldl plotAddErrorBar plotAddSurface plotCDFEmpirical plotSetColormap plotSetContourLabels plotSetLegendFont plotSetTextInterpreter plotSetXTicCount plotSetYTicCount plotSetZLevels powerm strjoin sylvester strtrim",
+literal:"DB_AFTER_LAST_ROW DB_ALL_TABLES DB_BATCH_OPERATIONS DB_BEFORE_FIRST_ROW DB_BLOB DB_EVENT_NOTIFICATIONS DB_FINISH_QUERY DB_HIGH_PRECISION DB_LAST_INSERT_ID DB_LOW_PRECISION_DOUBLE DB_LOW_PRECISION_INT32 DB_LOW_PRECISION_INT64 DB_LOW_PRECISION_NUMBERS DB_MULTIPLE_RESULT_SETS DB_NAMED_PLACEHOLDERS DB_POSITIONAL_PLACEHOLDERS DB_PREPARED_QUERIES DB_QUERY_SIZE DB_SIMPLE_LOCKING DB_SYSTEM_TABLES DB_TABLES DB_TRANSACTIONS DB_UNICODE DB_VIEWS __STDIN __STDOUT __STDERR __FILE_DIR"};AT_COMMENT_MODE=
+a.COMMENT("@","@");PREPROCESSOR={className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"define definecs|10 undef ifdef ifndef iflight ifdllcall ifmac ifos2win ifunix else endif lineson linesoff srcfile srcline"},contains:[{begin:/\\\n/,relevance:0},{beginKeywords:"include",end:"$",keywords:{"meta-keyword":"include"},contains:[{className:"meta-string",begin:'"',end:'"',illegal:"\\n"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,AT_COMMENT_MODE]};STRUCT_TYPE={begin:/\bstruct\s+/,end:/\s/,keywords:"struct",
+contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE,relevance:0}]};PARSE_PARAMS=[{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,endsWithParent:!0,relevance:0,contains:[{className:"literal",begin:/\.\.\./},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,AT_COMMENT_MODE,STRUCT_TYPE]}];FUNCTION_DEF={className:"title",begin:a.UNDERSCORE_IDENT_RE,relevance:0};DEFINITION=function(c,b,f){c=a.inherit({className:"function",beginKeywords:c,end:b,excludeEnd:!0,contains:[].concat(PARSE_PARAMS)},
+f||{});c.contains.push(FUNCTION_DEF);c.contains.push(a.C_NUMBER_MODE);c.contains.push(a.C_BLOCK_COMMENT_MODE);c.contains.push(AT_COMMENT_MODE);return c};BUILT_IN_REF={className:"built_in",begin:"\\b("+c.built_in.split(" ").join("|")+")\\b"};STRING_REF={className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE],relevance:0};FUNCTION_REF={begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,keywords:c,relevance:0,contains:[{beginKeywords:c.keyword},BUILT_IN_REF,{className:"built_in",begin:a.UNDERSCORE_IDENT_RE,
+relevance:0}]};FUNCTION_REF_PARAMS={begin:/\(/,end:/\)/,relevance:0,keywords:{built_in:c.built_in,literal:c.literal},contains:[a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,AT_COMMENT_MODE,BUILT_IN_REF,FUNCTION_REF,STRING_REF,"self"]};FUNCTION_REF.contains.push(FUNCTION_REF_PARAMS);return{aliases:["gss"],case_insensitive:!0,keywords:c,illegal:/(\{[%#]|[%#]\}| <- )/,contains:[a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,AT_COMMENT_MODE,STRING_REF,PREPROCESSOR,{className:"keyword",begin:/\bexternal (matrix|string|array|sparse matrix|struct|proc|keyword|fn)/},
+DEFINITION("proc keyword",";"),DEFINITION("fn","="),{beginKeywords:"for threadfor",end:/;/,relevance:0,contains:[a.C_BLOCK_COMMENT_MODE,AT_COMMENT_MODE,FUNCTION_REF_PARAMS]},{variants:[{begin:a.UNDERSCORE_IDENT_RE+"\\."+a.UNDERSCORE_IDENT_RE},{begin:a.UNDERSCORE_IDENT_RE+"\\s*="}],relevance:0},FUNCTION_REF,STRUCT_TYPE]}});b.registerLanguage("gcode",function(a){a=[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT(/\(/,/\)/),a.inherit(a.C_NUMBER_MODE,{begin:"([-+]?([0-9]*\\.?[0-9]+\\.?))|"+a.C_NUMBER_RE}),
+a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"name",begin:"([G])([0-9]+\\.?[0-9]?)"},{className:"name",begin:"([M])([0-9]+\\.?[0-9]?)"},{className:"attr",begin:"(VC|VS|#)",end:"(\\d+)"},{className:"attr",begin:"(VZOFX|VZOFY|VZOFZ)"},{className:"built_in",begin:"(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)",end:"([-+]?([0-9]*\\.?[0-9]+\\.?))(\\])"},{className:"symbol",variants:[{begin:"N",end:"\\d+",illegal:"\\W"}]}];return{aliases:["nc"],
+case_insensitive:!0,lexemes:"[A-Z_][A-Z0-9_.]*",keywords:"IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT EQ LT GT NE GE LE OR XOR",contains:[{className:"meta",begin:"\\%"},{className:"meta",begin:"([O])([0-9]+)"}].concat(a)}});b.registerLanguage("gherkin",function(a){return{aliases:["feature"],keywords:"Feature Background Ability Business Need Scenario Scenarios Scenario Outline Scenario Template Examples Given And Then But When",contains:[{className:"symbol",begin:"\\*",relevance:0},
+{className:"meta",begin:"@[^@\\s]+"},{begin:"\\|",end:"\\|\\w*$",contains:[{className:"string",begin:"[^|]+"}]},{className:"variable",begin:"<",end:">"},a.HASH_COMMENT_MODE,{className:"string",begin:'"""',end:'"""'},a.QUOTE_STRING_MODE]}});b.registerLanguage("glsl",function(a){return{keywords:{keyword:"break continue discard do else for if return while switch case default attribute binding buffer ccw centroid centroid varying coherent column_major const cw depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip triangles triangles_adjacency uniform varying vertices volatile writeonly",
 type:"atomic_uint bool bvec2 bvec3 bvec4 dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 double dvec2 dvec3 dvec4 float iimage1D iimage1DArray iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBufferiimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray image2DRect image3D imageBuffer imageCube imageCubeArray int isampler1D isampler1DArray isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 mat2 mat2x2 mat2x3 mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 sampler1D sampler1DArray sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow image1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect uimage3D uimageBuffer uimageCube uimageCubeArray uint usampler1D usampler1DArray usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D samplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 vec2 vec3 vec4 void",
 built_in:"gl_MaxAtomicCounterBindings gl_MaxAtomicCounterBufferSize gl_MaxClipDistances gl_MaxClipPlanes gl_MaxCombinedAtomicCounterBuffers gl_MaxCombinedAtomicCounters gl_MaxCombinedImageUniforms gl_MaxCombinedImageUnitsAndFragmentOutputs gl_MaxCombinedTextureImageUnits gl_MaxComputeAtomicCounterBuffers gl_MaxComputeAtomicCounters gl_MaxComputeImageUniforms gl_MaxComputeTextureImageUnits gl_MaxComputeUniformComponents gl_MaxComputeWorkGroupCount gl_MaxComputeWorkGroupSize gl_MaxDrawBuffers gl_MaxFragmentAtomicCounterBuffers gl_MaxFragmentAtomicCounters gl_MaxFragmentImageUniforms gl_MaxFragmentInputComponents gl_MaxFragmentInputVectors gl_MaxFragmentUniformComponents gl_MaxFragmentUniformVectors gl_MaxGeometryAtomicCounterBuffers gl_MaxGeometryAtomicCounters gl_MaxGeometryImageUniforms gl_MaxGeometryInputComponents gl_MaxGeometryOutputComponents gl_MaxGeometryOutputVertices gl_MaxGeometryTextureImageUnits gl_MaxGeometryTotalOutputComponents gl_MaxGeometryUniformComponents gl_MaxGeometryVaryingComponents gl_MaxImageSamples gl_MaxImageUnits gl_MaxLights gl_MaxPatchVertices gl_MaxProgramTexelOffset gl_MaxTessControlAtomicCounterBuffers gl_MaxTessControlAtomicCounters gl_MaxTessControlImageUniforms gl_MaxTessControlInputComponents gl_MaxTessControlOutputComponents gl_MaxTessControlTextureImageUnits gl_MaxTessControlTotalOutputComponents gl_MaxTessControlUniformComponents gl_MaxTessEvaluationAtomicCounterBuffers gl_MaxTessEvaluationAtomicCounters gl_MaxTessEvaluationImageUniforms gl_MaxTessEvaluationInputComponents gl_MaxTessEvaluationOutputComponents gl_MaxTessEvaluationTextureImageUnits gl_MaxTessEvaluationUniformComponents gl_MaxTessGenLevel gl_MaxTessPatchComponents gl_MaxTextureCoords gl_MaxTextureImageUnits gl_MaxTextureUnits gl_MaxVaryingComponents gl_MaxVaryingFloats gl_MaxVaryingVectors gl_MaxVertexAtomicCounterBuffers gl_MaxVertexAtomicCounters gl_MaxVertexAttribs gl_MaxVertexImageUniforms gl_MaxVertexOutputComponents gl_MaxVertexOutputVectors gl_MaxVertexTextureImageUnits gl_MaxVertexUniformComponents gl_MaxVertexUniformVectors gl_MaxViewports gl_MinProgramTexelOffset gl_BackColor gl_BackLightModelProduct gl_BackLightProduct gl_BackMaterial gl_BackSecondaryColor gl_ClipDistance gl_ClipPlane gl_ClipVertex gl_Color gl_DepthRange gl_EyePlaneQ gl_EyePlaneR gl_EyePlaneS gl_EyePlaneT gl_Fog gl_FogCoord gl_FogFragCoord gl_FragColor gl_FragCoord gl_FragData gl_FragDepth gl_FrontColor gl_FrontFacing gl_FrontLightModelProduct gl_FrontLightProduct gl_FrontMaterial gl_FrontSecondaryColor gl_GlobalInvocationID gl_InstanceID gl_InvocationID gl_Layer gl_LightModel gl_LightSource gl_LocalInvocationID gl_LocalInvocationIndex gl_ModelViewMatrix gl_ModelViewMatrixInverse gl_ModelViewMatrixInverseTranspose gl_ModelViewMatrixTranspose gl_ModelViewProjectionMatrix gl_ModelViewProjectionMatrixInverse gl_ModelViewProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixTranspose gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_Normal gl_NormalMatrix gl_NormalScale gl_NumSamples gl_NumWorkGroups gl_ObjectPlaneQ gl_ObjectPlaneR gl_ObjectPlaneS gl_ObjectPlaneT gl_PatchVerticesIn gl_Point gl_PointCoord gl_PointSize gl_Position gl_PrimitiveID gl_PrimitiveIDIn gl_ProjectionMatrix gl_ProjectionMatrixInverse gl_ProjectionMatrixInverseTranspose gl_ProjectionMatrixTranspose gl_SampleID gl_SampleMask gl_SampleMaskIn gl_SamplePosition gl_SecondaryColor gl_TessCoord gl_TessLevelInner gl_TessLevelOuter gl_TexCoord gl_TextureEnvColor gl_TextureMatrix gl_TextureMatrixInverse gl_TextureMatrixInverseTranspose gl_TextureMatrixTranspose gl_Vertex gl_VertexID gl_ViewportIndex gl_WorkGroupID gl_WorkGroupSize gl_in gl_out EmitStreamVertex EmitVertex EndPrimitive EndStreamPrimitive abs acos acosh all any asin asinh atan atanh atomicAdd atomicAnd atomicCompSwap atomicCounter atomicCounterDecrement atomicCounterIncrement atomicExchange atomicMax atomicMin atomicOr atomicXor barrier bitCount bitfieldExtract bitfieldInsert bitfieldReverse ceil clamp cos cosh cross dFdx dFdy degrees determinant distance dot equal exp exp2 faceforward findLSB findMSB floatBitsToInt floatBitsToUint floor fma fract frexp ftransform fwidth greaterThan greaterThanEqual groupMemoryBarrier imageAtomicAdd imageAtomicAnd imageAtomicCompSwap imageAtomicExchange imageAtomicMax imageAtomicMin imageAtomicOr imageAtomicXor imageLoad imageSize imageStore imulExtended intBitsToFloat interpolateAtCentroid interpolateAtOffset interpolateAtSample inverse inversesqrt isinf isnan ldexp length lessThan lessThanEqual log log2 matrixCompMult max memoryBarrier memoryBarrierAtomicCounter memoryBarrierBuffer memoryBarrierImage memoryBarrierShared min mix mod modf noise1 noise2 noise3 noise4 normalize not notEqual outerProduct packDouble2x32 packHalf2x16 packSnorm2x16 packSnorm4x8 packUnorm2x16 packUnorm4x8 pow radians reflect refract round roundEven shadow1D shadow1DLod shadow1DProj shadow1DProjLod shadow2D shadow2DLod shadow2DProj shadow2DProjLod sign sin sinh smoothstep sqrt step tan tanh texelFetch texelFetchOffset texture texture1D texture1DLod texture1DProj texture1DProjLod texture2D texture2DLod texture2DProj texture2DProjLod texture3D texture3DLod texture3DProj texture3DProjLod textureCube textureCubeLod textureGather textureGatherOffset textureGatherOffsets textureGrad textureGradOffset textureLod textureLodOffset textureOffset textureProj textureProjGrad textureProjGradOffset textureProjLod textureProjLodOffset textureProjOffset textureQueryLevels textureQueryLod textureSize transpose trunc uaddCarry uintBitsToFloat umulExtended unpackDouble2x32 unpackHalf2x16 unpackSnorm2x16 unpackSnorm4x8 unpackUnorm2x16 unpackUnorm4x8 usubBorrow",
-literal:"true false"},illegal:'"',contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$"}]}});b.registerLanguage("go",function(a){var c={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",
-built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],keywords:c,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0,contains:[a.TITLE_MODE,
-{className:"params",begin:/\(/,end:/\)/,keywords:c,illegal:/["']/}]}]}});b.registerLanguage("golo",function(a){return{keywords:{keyword:"println readln print import module function local return let var while for foreach times in case when match with break continue augment augmentation each find filter reduce if then else otherwise try catch finally raise throw orIfNull DynamicObject|10 DynamicVariable struct Observable map set vector list array",literal:"true false null"},contains:[a.HASH_COMMENT_MODE,
-a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("gradle",function(a){return{case_insensitive:!0,keywords:{keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"},
+literal:"true false"},illegal:'"',contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"#",end:"$"}]}});b.registerLanguage("gml",function(a){return{aliases:["gml","GML"],case_insensitive:!1,keywords:{keywords:"begin end if then else while do for break continue with until repeat exit and or xor not return mod div switch case default var globalvar enum #macro #region #endregion",built_in:"is_real is_string is_array is_undefined is_int32 is_int64 is_ptr is_vec3 is_vec4 is_matrix is_bool typeof variable_global_exists variable_global_get variable_global_set variable_instance_exists variable_instance_get variable_instance_set variable_instance_get_names array_length_1d array_length_2d array_height_2d array_equals array_create array_copy random random_range irandom irandom_range random_set_seed random_get_seed randomize randomise choose abs round floor ceil sign frac sqrt sqr exp ln log2 log10 sin cos tan arcsin arccos arctan arctan2 dsin dcos dtan darcsin darccos darctan darctan2 degtorad radtodeg power logn min max mean median clamp lerp dot_product dot_product_3d dot_product_normalised dot_product_3d_normalised dot_product_normalized dot_product_3d_normalized math_set_epsilon math_get_epsilon angle_difference point_distance_3d point_distance point_direction lengthdir_x lengthdir_y real string int64 ptr string_format chr ansi_char ord string_length string_byte_length string_pos string_copy string_char_at string_ord_at string_byte_at string_set_byte_at string_delete string_insert string_lower string_upper string_repeat string_letters string_digits string_lettersdigits string_replace string_replace_all string_count string_hash_to_newline clipboard_has_text clipboard_set_text clipboard_get_text date_current_datetime date_create_datetime date_valid_datetime date_inc_year date_inc_month date_inc_week date_inc_day date_inc_hour date_inc_minute date_inc_second date_get_year date_get_month date_get_week date_get_day date_get_hour date_get_minute date_get_second date_get_weekday date_get_day_of_year date_get_hour_of_year date_get_minute_of_year date_get_second_of_year date_year_span date_month_span date_week_span date_day_span date_hour_span date_minute_span date_second_span date_compare_datetime date_compare_date date_compare_time date_date_of date_time_of date_datetime_string date_date_string date_time_string date_days_in_month date_days_in_year date_leap_year date_is_today date_set_timezone date_get_timezone game_set_speed game_get_speed motion_set motion_add place_free place_empty place_meeting place_snapped move_random move_snap move_towards_point move_contact_solid move_contact_all move_outside_solid move_outside_all move_bounce_solid move_bounce_all move_wrap distance_to_point distance_to_object position_empty position_meeting path_start path_end mp_linear_step mp_potential_step mp_linear_step_object mp_potential_step_object mp_potential_settings mp_linear_path mp_potential_path mp_linear_path_object mp_potential_path_object mp_grid_create mp_grid_destroy mp_grid_clear_all mp_grid_clear_cell mp_grid_clear_rectangle mp_grid_add_cell mp_grid_get_cell mp_grid_add_rectangle mp_grid_add_instances mp_grid_path mp_grid_draw mp_grid_to_ds_grid collision_point collision_rectangle collision_circle collision_ellipse collision_line collision_point_list collision_rectangle_list collision_circle_list collision_ellipse_list collision_line_list instance_position_list instance_place_list point_in_rectangle point_in_triangle point_in_circle rectangle_in_rectangle rectangle_in_triangle rectangle_in_circle instance_find instance_exists instance_number instance_position instance_nearest instance_furthest instance_place instance_create_depth instance_create_layer instance_copy instance_change instance_destroy position_destroy position_change instance_id_get instance_deactivate_all instance_deactivate_object instance_deactivate_region instance_activate_all instance_activate_object instance_activate_region room_goto room_goto_previous room_goto_next room_previous room_next room_restart game_end game_restart game_load game_save game_save_buffer game_load_buffer event_perform event_user event_perform_object event_inherited show_debug_message show_debug_overlay debug_event debug_get_callstack alarm_get alarm_set font_texture_page_size keyboard_set_map keyboard_get_map keyboard_unset_map keyboard_check keyboard_check_pressed keyboard_check_released keyboard_check_direct keyboard_get_numlock keyboard_set_numlock keyboard_key_press keyboard_key_release keyboard_clear io_clear mouse_check_button mouse_check_button_pressed mouse_check_button_released mouse_wheel_up mouse_wheel_down mouse_clear draw_self draw_sprite draw_sprite_pos draw_sprite_ext draw_sprite_stretched draw_sprite_stretched_ext draw_sprite_tiled draw_sprite_tiled_ext draw_sprite_part draw_sprite_part_ext draw_sprite_general draw_clear draw_clear_alpha draw_point draw_line draw_line_width draw_rectangle draw_roundrect draw_roundrect_ext draw_triangle draw_circle draw_ellipse draw_set_circle_precision draw_arrow draw_button draw_path draw_healthbar draw_getpixel draw_getpixel_ext draw_set_colour draw_set_color draw_set_alpha draw_get_colour draw_get_color draw_get_alpha merge_colour make_colour_rgb make_colour_hsv colour_get_red colour_get_green colour_get_blue colour_get_hue colour_get_saturation colour_get_value merge_color make_color_rgb make_color_hsv color_get_red color_get_green color_get_blue color_get_hue color_get_saturation color_get_value merge_color screen_save screen_save_part draw_set_font draw_set_halign draw_set_valign draw_text draw_text_ext string_width string_height string_width_ext string_height_ext draw_text_transformed draw_text_ext_transformed draw_text_colour draw_text_ext_colour draw_text_transformed_colour draw_text_ext_transformed_colour draw_text_color draw_text_ext_color draw_text_transformed_color draw_text_ext_transformed_color draw_point_colour draw_line_colour draw_line_width_colour draw_rectangle_colour draw_roundrect_colour draw_roundrect_colour_ext draw_triangle_colour draw_circle_colour draw_ellipse_colour draw_point_color draw_line_color draw_line_width_color draw_rectangle_color draw_roundrect_color draw_roundrect_color_ext draw_triangle_color draw_circle_color draw_ellipse_color draw_primitive_begin draw_vertex draw_vertex_colour draw_vertex_color draw_primitive_end sprite_get_uvs font_get_uvs sprite_get_texture font_get_texture texture_get_width texture_get_height texture_get_uvs draw_primitive_begin_texture draw_vertex_texture draw_vertex_texture_colour draw_vertex_texture_color texture_global_scale surface_create surface_create_ext surface_resize surface_free surface_exists surface_get_width surface_get_height surface_get_texture surface_set_target surface_set_target_ext surface_reset_target surface_depth_disable surface_get_depth_disable draw_surface draw_surface_stretched draw_surface_tiled draw_surface_part draw_surface_ext draw_surface_stretched_ext draw_surface_tiled_ext draw_surface_part_ext draw_surface_general surface_getpixel surface_getpixel_ext surface_save surface_save_part surface_copy surface_copy_part application_surface_draw_enable application_get_position application_surface_enable application_surface_is_enabled display_get_width display_get_height display_get_orientation display_get_gui_width display_get_gui_height display_reset display_mouse_get_x display_mouse_get_y display_mouse_set display_set_ui_visibility window_set_fullscreen window_get_fullscreen window_set_caption window_set_min_width window_set_max_width window_set_min_height window_set_max_height window_get_visible_rects window_get_caption window_set_cursor window_get_cursor window_set_colour window_get_colour window_set_color window_get_color window_set_position window_set_size window_set_rectangle window_center window_get_x window_get_y window_get_width window_get_height window_mouse_get_x window_mouse_get_y window_mouse_set window_view_mouse_get_x window_view_mouse_get_y window_views_mouse_get_x window_views_mouse_get_y audio_listener_position audio_listener_velocity audio_listener_orientation audio_emitter_position audio_emitter_create audio_emitter_free audio_emitter_exists audio_emitter_pitch audio_emitter_velocity audio_emitter_falloff audio_emitter_gain audio_play_sound audio_play_sound_on audio_play_sound_at audio_stop_sound audio_resume_music audio_music_is_playing audio_resume_sound audio_pause_sound audio_pause_music audio_channel_num audio_sound_length audio_get_type audio_falloff_set_model audio_play_music audio_stop_music audio_master_gain audio_music_gain audio_sound_gain audio_sound_pitch audio_stop_all audio_resume_all audio_pause_all audio_is_playing audio_is_paused audio_exists audio_sound_set_track_position audio_sound_get_track_position audio_emitter_get_gain audio_emitter_get_pitch audio_emitter_get_x audio_emitter_get_y audio_emitter_get_z audio_emitter_get_vx audio_emitter_get_vy audio_emitter_get_vz audio_listener_set_position audio_listener_set_velocity audio_listener_set_orientation audio_listener_get_data audio_set_master_gain audio_get_master_gain audio_sound_get_gain audio_sound_get_pitch audio_get_name audio_sound_set_track_position audio_sound_get_track_position audio_create_stream audio_destroy_stream audio_create_sync_group audio_destroy_sync_group audio_play_in_sync_group audio_start_sync_group audio_stop_sync_group audio_pause_sync_group audio_resume_sync_group audio_sync_group_get_track_pos audio_sync_group_debug audio_sync_group_is_playing audio_debug audio_group_load audio_group_unload audio_group_is_loaded audio_group_load_progress audio_group_name audio_group_stop_all audio_group_set_gain audio_create_buffer_sound audio_free_buffer_sound audio_create_play_queue audio_free_play_queue audio_queue_sound audio_get_recorder_count audio_get_recorder_info audio_start_recording audio_stop_recording audio_sound_get_listener_mask audio_emitter_get_listener_mask audio_get_listener_mask audio_sound_set_listener_mask audio_emitter_set_listener_mask audio_set_listener_mask audio_get_listener_count audio_get_listener_info audio_system show_message show_message_async clickable_add clickable_add_ext clickable_change clickable_change_ext clickable_delete clickable_exists clickable_set_style show_question show_question_async get_integer get_string get_integer_async get_string_async get_login_async get_open_filename get_save_filename get_open_filename_ext get_save_filename_ext show_error highscore_clear highscore_add highscore_value highscore_name draw_highscore sprite_exists sprite_get_name sprite_get_number sprite_get_width sprite_get_height sprite_get_xoffset sprite_get_yoffset sprite_get_bbox_left sprite_get_bbox_right sprite_get_bbox_top sprite_get_bbox_bottom sprite_save sprite_save_strip sprite_set_cache_size sprite_set_cache_size_ext sprite_get_tpe sprite_prefetch sprite_prefetch_multi sprite_flush sprite_flush_multi sprite_set_speed sprite_get_speed_type sprite_get_speed font_exists font_get_name font_get_fontname font_get_bold font_get_italic font_get_first font_get_last font_get_size font_set_cache_size path_exists path_get_name path_get_length path_get_time path_get_kind path_get_closed path_get_precision path_get_number path_get_point_x path_get_point_y path_get_point_speed path_get_x path_get_y path_get_speed script_exists script_get_name timeline_add timeline_delete timeline_clear timeline_exists timeline_get_name timeline_moment_clear timeline_moment_add_script timeline_size timeline_max_moment object_exists object_get_name object_get_sprite object_get_solid object_get_visible object_get_persistent object_get_mask object_get_parent object_get_physics object_is_ancestor room_exists room_get_name sprite_set_offset sprite_duplicate sprite_assign sprite_merge sprite_add sprite_replace sprite_create_from_surface sprite_add_from_surface sprite_delete sprite_set_alpha_from_sprite sprite_collision_mask font_add_enable_aa font_add_get_enable_aa font_add font_add_sprite font_add_sprite_ext font_replace font_replace_sprite font_replace_sprite_ext font_delete path_set_kind path_set_closed path_set_precision path_add path_assign path_duplicate path_append path_delete path_add_point path_insert_point path_change_point path_delete_point path_clear_points path_reverse path_mirror path_flip path_rotate path_rescale path_shift script_execute object_set_sprite object_set_solid object_set_visible object_set_persistent object_set_mask room_set_width room_set_height room_set_persistent room_set_background_colour room_set_background_color room_set_view room_set_viewport room_get_viewport room_set_view_enabled room_add room_duplicate room_assign room_instance_add room_instance_clear room_get_camera room_set_camera asset_get_index asset_get_type file_text_open_from_string file_text_open_read file_text_open_write file_text_open_append file_text_close file_text_write_string file_text_write_real file_text_writeln file_text_read_string file_text_read_real file_text_readln file_text_eof file_text_eoln file_exists file_delete file_rename file_copy directory_exists directory_create directory_destroy file_find_first file_find_next file_find_close file_attributes filename_name filename_path filename_dir filename_drive filename_ext filename_change_ext file_bin_open file_bin_rewrite file_bin_close file_bin_position file_bin_size file_bin_seek file_bin_write_byte file_bin_read_byte parameter_count parameter_string environment_get_variable ini_open_from_string ini_open ini_close ini_read_string ini_read_real ini_write_string ini_write_real ini_key_exists ini_section_exists ini_key_delete ini_section_delete ds_set_precision ds_exists ds_stack_create ds_stack_destroy ds_stack_clear ds_stack_copy ds_stack_size ds_stack_empty ds_stack_push ds_stack_pop ds_stack_top ds_stack_write ds_stack_read ds_queue_create ds_queue_destroy ds_queue_clear ds_queue_copy ds_queue_size ds_queue_empty ds_queue_enqueue ds_queue_dequeue ds_queue_head ds_queue_tail ds_queue_write ds_queue_read ds_list_create ds_list_destroy ds_list_clear ds_list_copy ds_list_size ds_list_empty ds_list_add ds_list_insert ds_list_replace ds_list_delete ds_list_find_index ds_list_find_value ds_list_mark_as_list ds_list_mark_as_map ds_list_sort ds_list_shuffle ds_list_write ds_list_read ds_list_set ds_map_create ds_map_destroy ds_map_clear ds_map_copy ds_map_size ds_map_empty ds_map_add ds_map_add_list ds_map_add_map ds_map_replace ds_map_replace_map ds_map_replace_list ds_map_delete ds_map_exists ds_map_find_value ds_map_find_previous ds_map_find_next ds_map_find_first ds_map_find_last ds_map_write ds_map_read ds_map_secure_save ds_map_secure_load ds_map_secure_load_buffer ds_map_secure_save_buffer ds_map_set ds_priority_create ds_priority_destroy ds_priority_clear ds_priority_copy ds_priority_size ds_priority_empty ds_priority_add ds_priority_change_priority ds_priority_find_priority ds_priority_delete_value ds_priority_delete_min ds_priority_find_min ds_priority_delete_max ds_priority_find_max ds_priority_write ds_priority_read ds_grid_create ds_grid_destroy ds_grid_copy ds_grid_resize ds_grid_width ds_grid_height ds_grid_clear ds_grid_set ds_grid_add ds_grid_multiply ds_grid_set_region ds_grid_add_region ds_grid_multiply_region ds_grid_set_disk ds_grid_add_disk ds_grid_multiply_disk ds_grid_set_grid_region ds_grid_add_grid_region ds_grid_multiply_grid_region ds_grid_get ds_grid_get_sum ds_grid_get_max ds_grid_get_min ds_grid_get_mean ds_grid_get_disk_sum ds_grid_get_disk_min ds_grid_get_disk_max ds_grid_get_disk_mean ds_grid_value_exists ds_grid_value_x ds_grid_value_y ds_grid_value_disk_exists ds_grid_value_disk_x ds_grid_value_disk_y ds_grid_shuffle ds_grid_write ds_grid_read ds_grid_sort ds_grid_set ds_grid_get effect_create_below effect_create_above effect_clear part_type_create part_type_destroy part_type_exists part_type_clear part_type_shape part_type_sprite part_type_size part_type_scale part_type_orientation part_type_life part_type_step part_type_death part_type_speed part_type_direction part_type_gravity part_type_colour1 part_type_colour2 part_type_colour3 part_type_colour_mix part_type_colour_rgb part_type_colour_hsv part_type_color1 part_type_color2 part_type_color3 part_type_color_mix part_type_color_rgb part_type_color_hsv part_type_alpha1 part_type_alpha2 part_type_alpha3 part_type_blend part_system_create part_system_create_layer part_system_destroy part_system_exists part_system_clear part_system_draw_order part_system_depth part_system_position part_system_automatic_update part_system_automatic_draw part_system_update part_system_drawit part_system_get_layer part_system_layer part_particles_create part_particles_create_colour part_particles_create_color part_particles_clear part_particles_count part_emitter_create part_emitter_destroy part_emitter_destroy_all part_emitter_exists part_emitter_clear part_emitter_region part_emitter_burst part_emitter_stream external_call external_define external_free window_handle window_device matrix_get matrix_set matrix_build_identity matrix_build matrix_build_lookat matrix_build_projection_ortho matrix_build_projection_perspective matrix_build_projection_perspective_fov matrix_multiply matrix_transform_vertex matrix_stack_push matrix_stack_pop matrix_stack_multiply matrix_stack_set matrix_stack_clear matrix_stack_top matrix_stack_is_empty browser_input_capture os_get_config os_get_info os_get_language os_get_region os_lock_orientation display_get_dpi_x display_get_dpi_y display_set_gui_size display_set_gui_maximise display_set_gui_maximize device_mouse_dbclick_enable display_set_timing_method display_get_timing_method display_set_sleep_margin display_get_sleep_margin virtual_key_add virtual_key_hide virtual_key_delete virtual_key_show draw_enable_drawevent draw_enable_swf_aa draw_set_swf_aa_level draw_get_swf_aa_level draw_texture_flush draw_flush gpu_set_blendenable gpu_set_ztestenable gpu_set_zfunc gpu_set_zwriteenable gpu_set_lightingenable gpu_set_fog gpu_set_cullmode gpu_set_blendmode gpu_set_blendmode_ext gpu_set_blendmode_ext_sepalpha gpu_set_colorwriteenable gpu_set_colourwriteenable gpu_set_alphatestenable gpu_set_alphatestref gpu_set_alphatestfunc gpu_set_texfilter gpu_set_texfilter_ext gpu_set_texrepeat gpu_set_texrepeat_ext gpu_set_tex_filter gpu_set_tex_filter_ext gpu_set_tex_repeat gpu_set_tex_repeat_ext gpu_set_tex_mip_filter gpu_set_tex_mip_filter_ext gpu_set_tex_mip_bias gpu_set_tex_mip_bias_ext gpu_set_tex_min_mip gpu_set_tex_min_mip_ext gpu_set_tex_max_mip gpu_set_tex_max_mip_ext gpu_set_tex_max_aniso gpu_set_tex_max_aniso_ext gpu_set_tex_mip_enable gpu_set_tex_mip_enable_ext gpu_get_blendenable gpu_get_ztestenable gpu_get_zfunc gpu_get_zwriteenable gpu_get_lightingenable gpu_get_fog gpu_get_cullmode gpu_get_blendmode gpu_get_blendmode_ext gpu_get_blendmode_ext_sepalpha gpu_get_blendmode_src gpu_get_blendmode_dest gpu_get_blendmode_srcalpha gpu_get_blendmode_destalpha gpu_get_colorwriteenable gpu_get_colourwriteenable gpu_get_alphatestenable gpu_get_alphatestref gpu_get_alphatestfunc gpu_get_texfilter gpu_get_texfilter_ext gpu_get_texrepeat gpu_get_texrepeat_ext gpu_get_tex_filter gpu_get_tex_filter_ext gpu_get_tex_repeat gpu_get_tex_repeat_ext gpu_get_tex_mip_filter gpu_get_tex_mip_filter_ext gpu_get_tex_mip_bias gpu_get_tex_mip_bias_ext gpu_get_tex_min_mip gpu_get_tex_min_mip_ext gpu_get_tex_max_mip gpu_get_tex_max_mip_ext gpu_get_tex_max_aniso gpu_get_tex_max_aniso_ext gpu_get_tex_mip_enable gpu_get_tex_mip_enable_ext gpu_push_state gpu_pop_state gpu_get_state gpu_set_state draw_light_define_ambient draw_light_define_direction draw_light_define_point draw_light_enable draw_set_lighting draw_light_get_ambient draw_light_get draw_get_lighting shop_leave_rating url_get_domain url_open url_open_ext url_open_full get_timer achievement_login achievement_logout achievement_post achievement_increment achievement_post_score achievement_available achievement_show_achievements achievement_show_leaderboards achievement_load_friends achievement_load_leaderboard achievement_send_challenge achievement_load_progress achievement_reset achievement_login_status achievement_get_pic achievement_show_challenge_notifications achievement_get_challenges achievement_event achievement_show achievement_get_info cloud_file_save cloud_string_save cloud_synchronise ads_enable ads_disable ads_setup ads_engagement_launch ads_engagement_available ads_engagement_active ads_event ads_event_preload ads_set_reward_callback ads_get_display_height ads_get_display_width ads_move ads_interstitial_available ads_interstitial_display device_get_tilt_x device_get_tilt_y device_get_tilt_z device_is_keypad_open device_mouse_check_button device_mouse_check_button_pressed device_mouse_check_button_released device_mouse_x device_mouse_y device_mouse_raw_x device_mouse_raw_y device_mouse_x_to_gui device_mouse_y_to_gui iap_activate iap_status iap_enumerate_products iap_restore_all iap_acquire iap_consume iap_product_details iap_purchase_details facebook_init facebook_login facebook_status facebook_graph_request facebook_dialog facebook_logout facebook_launch_offerwall facebook_post_message facebook_send_invite facebook_user_id facebook_accesstoken facebook_check_permission facebook_request_read_permissions facebook_request_publish_permissions gamepad_is_supported gamepad_get_device_count gamepad_is_connected gamepad_get_description gamepad_get_button_threshold gamepad_set_button_threshold gamepad_get_axis_deadzone gamepad_set_axis_deadzone gamepad_button_count gamepad_button_check gamepad_button_check_pressed gamepad_button_check_released gamepad_button_value gamepad_axis_count gamepad_axis_value gamepad_set_vibration gamepad_set_colour gamepad_set_color os_is_paused window_has_focus code_is_compiled http_get http_get_file http_post_string http_request json_encode json_decode zip_unzip load_csv base64_encode base64_decode md5_string_unicode md5_string_utf8 md5_file os_is_network_connected sha1_string_unicode sha1_string_utf8 sha1_file os_powersave_enable analytics_event analytics_event_ext win8_livetile_tile_notification win8_livetile_tile_clear win8_livetile_badge_notification win8_livetile_badge_clear win8_livetile_queue_enable win8_secondarytile_pin win8_secondarytile_badge_notification win8_secondarytile_delete win8_livetile_notification_begin win8_livetile_notification_secondary_begin win8_livetile_notification_expiry win8_livetile_notification_tag win8_livetile_notification_text_add win8_livetile_notification_image_add win8_livetile_notification_end win8_appbar_enable win8_appbar_add_element win8_appbar_remove_element win8_settingscharm_add_entry win8_settingscharm_add_html_entry win8_settingscharm_add_xaml_entry win8_settingscharm_set_xaml_property win8_settingscharm_get_xaml_property win8_settingscharm_remove_entry win8_share_image win8_share_screenshot win8_share_file win8_share_url win8_share_text win8_search_enable win8_search_disable win8_search_add_suggestions win8_device_touchscreen_available win8_license_initialize_sandbox win8_license_trial_version winphone_license_trial_version winphone_tile_title winphone_tile_count winphone_tile_back_title winphone_tile_back_content winphone_tile_back_content_wide winphone_tile_front_image winphone_tile_front_image_small winphone_tile_front_image_wide winphone_tile_back_image winphone_tile_back_image_wide winphone_tile_background_colour winphone_tile_background_color winphone_tile_icon_image winphone_tile_small_icon_image winphone_tile_wide_content winphone_tile_cycle_images winphone_tile_small_background_image physics_world_create physics_world_gravity physics_world_update_speed physics_world_update_iterations physics_world_draw_debug physics_pause_enable physics_fixture_create physics_fixture_set_kinematic physics_fixture_set_density physics_fixture_set_awake physics_fixture_set_restitution physics_fixture_set_friction physics_fixture_set_collision_group physics_fixture_set_sensor physics_fixture_set_linear_damping physics_fixture_set_angular_damping physics_fixture_set_circle_shape physics_fixture_set_box_shape physics_fixture_set_edge_shape physics_fixture_set_polygon_shape physics_fixture_set_chain_shape physics_fixture_add_point physics_fixture_bind physics_fixture_bind_ext physics_fixture_delete physics_apply_force physics_apply_impulse physics_apply_angular_impulse physics_apply_local_force physics_apply_local_impulse physics_apply_torque physics_mass_properties physics_draw_debug physics_test_overlap physics_remove_fixture physics_set_friction physics_set_density physics_set_restitution physics_get_friction physics_get_density physics_get_restitution physics_joint_distance_create physics_joint_rope_create physics_joint_revolute_create physics_joint_prismatic_create physics_joint_pulley_create physics_joint_wheel_create physics_joint_weld_create physics_joint_friction_create physics_joint_gear_create physics_joint_enable_motor physics_joint_get_value physics_joint_set_value physics_joint_delete physics_particle_create physics_particle_delete physics_particle_delete_region_circle physics_particle_delete_region_box physics_particle_delete_region_poly physics_particle_set_flags physics_particle_set_category_flags physics_particle_draw physics_particle_draw_ext physics_particle_count physics_particle_get_data physics_particle_get_data_particle physics_particle_group_begin physics_particle_group_circle physics_particle_group_box physics_particle_group_polygon physics_particle_group_add_point physics_particle_group_end physics_particle_group_join physics_particle_group_delete physics_particle_group_count physics_particle_group_get_data physics_particle_group_get_mass physics_particle_group_get_inertia physics_particle_group_get_centre_x physics_particle_group_get_centre_y physics_particle_group_get_vel_x physics_particle_group_get_vel_y physics_particle_group_get_ang_vel physics_particle_group_get_x physics_particle_group_get_y physics_particle_group_get_angle physics_particle_set_group_flags physics_particle_get_group_flags physics_particle_get_max_count physics_particle_get_radius physics_particle_get_density physics_particle_get_damping physics_particle_get_gravity_scale physics_particle_set_max_count physics_particle_set_radius physics_particle_set_density physics_particle_set_damping physics_particle_set_gravity_scale network_create_socket network_create_socket_ext network_create_server network_create_server_raw network_connect network_connect_raw network_send_packet network_send_raw network_send_broadcast network_send_udp network_send_udp_raw network_set_timeout network_set_config network_resolve network_destroy buffer_create buffer_write buffer_read buffer_seek buffer_get_surface buffer_set_surface buffer_delete buffer_exists buffer_get_type buffer_get_alignment buffer_poke buffer_peek buffer_save buffer_save_ext buffer_load buffer_load_ext buffer_load_partial buffer_copy buffer_fill buffer_get_size buffer_tell buffer_resize buffer_md5 buffer_sha1 buffer_base64_encode buffer_base64_decode buffer_base64_decode_ext buffer_sizeof buffer_get_address buffer_create_from_vertex_buffer buffer_create_from_vertex_buffer_ext buffer_copy_from_vertex_buffer buffer_async_group_begin buffer_async_group_option buffer_async_group_end buffer_load_async buffer_save_async gml_release_mode gml_pragma steam_activate_overlay steam_is_overlay_enabled steam_is_overlay_activated steam_get_persona_name steam_initialised steam_is_cloud_enabled_for_app steam_is_cloud_enabled_for_account steam_file_persisted steam_get_quota_total steam_get_quota_free steam_file_write steam_file_write_file steam_file_read steam_file_delete steam_file_exists steam_file_size steam_file_share steam_is_screenshot_requested steam_send_screenshot steam_is_user_logged_on steam_get_user_steam_id steam_user_owns_dlc steam_user_installed_dlc steam_set_achievement steam_get_achievement steam_clear_achievement steam_set_stat_int steam_set_stat_float steam_set_stat_avg_rate steam_get_stat_int steam_get_stat_float steam_get_stat_avg_rate steam_reset_all_stats steam_reset_all_stats_achievements steam_stats_ready steam_create_leaderboard steam_upload_score steam_upload_score_ext steam_download_scores_around_user steam_download_scores steam_download_friends_scores steam_upload_score_buffer steam_upload_score_buffer_ext steam_current_game_language steam_available_languages steam_activate_overlay_browser steam_activate_overlay_user steam_activate_overlay_store steam_get_user_persona_name steam_get_app_id steam_get_user_account_id steam_ugc_download steam_ugc_create_item steam_ugc_start_item_update steam_ugc_set_item_title steam_ugc_set_item_description steam_ugc_set_item_visibility steam_ugc_set_item_tags steam_ugc_set_item_content steam_ugc_set_item_preview steam_ugc_submit_item_update steam_ugc_get_item_update_progress steam_ugc_subscribe_item steam_ugc_unsubscribe_item steam_ugc_num_subscribed_items steam_ugc_get_subscribed_items steam_ugc_get_item_install_info steam_ugc_get_item_update_info steam_ugc_request_item_details steam_ugc_create_query_user steam_ugc_create_query_user_ex steam_ugc_create_query_all steam_ugc_create_query_all_ex steam_ugc_query_set_cloud_filename_filter steam_ugc_query_set_match_any_tag steam_ugc_query_set_search_text steam_ugc_query_set_ranked_by_trend_days steam_ugc_query_add_required_tag steam_ugc_query_add_excluded_tag steam_ugc_query_set_return_long_description steam_ugc_query_set_return_total_only steam_ugc_query_set_allow_cached_response steam_ugc_send_query shader_set shader_get_name shader_reset shader_current shader_is_compiled shader_get_sampler_index shader_get_uniform shader_set_uniform_i shader_set_uniform_i_array shader_set_uniform_f shader_set_uniform_f_array shader_set_uniform_matrix shader_set_uniform_matrix_array shader_enable_corner_id texture_set_stage texture_get_texel_width texture_get_texel_height shaders_are_supported vertex_format_begin vertex_format_end vertex_format_delete vertex_format_add_position vertex_format_add_position_3d vertex_format_add_colour vertex_format_add_color vertex_format_add_normal vertex_format_add_texcoord vertex_format_add_textcoord vertex_format_add_custom vertex_create_buffer vertex_create_buffer_ext vertex_delete_buffer vertex_begin vertex_end vertex_position vertex_position_3d vertex_colour vertex_color vertex_argb vertex_texcoord vertex_normal vertex_float1 vertex_float2 vertex_float3 vertex_float4 vertex_ubyte4 vertex_submit vertex_freeze vertex_get_number vertex_get_buffer_size vertex_create_buffer_from_buffer vertex_create_buffer_from_buffer_ext push_local_notification push_get_first_local_notification push_get_next_local_notification push_cancel_local_notification skeleton_animation_set skeleton_animation_get skeleton_animation_mix skeleton_animation_set_ext skeleton_animation_get_ext skeleton_animation_get_duration skeleton_animation_get_frames skeleton_animation_clear skeleton_skin_set skeleton_skin_get skeleton_attachment_set skeleton_attachment_get skeleton_attachment_create skeleton_collision_draw_set skeleton_bone_data_get skeleton_bone_data_set skeleton_bone_state_get skeleton_bone_state_set skeleton_get_minmax skeleton_get_num_bounds skeleton_get_bounds skeleton_animation_get_frame skeleton_animation_set_frame draw_skeleton draw_skeleton_time draw_skeleton_instance draw_skeleton_collision skeleton_animation_list skeleton_skin_list skeleton_slot_data layer_get_id layer_get_id_at_depth layer_get_depth layer_create layer_destroy layer_destroy_instances layer_add_instance layer_has_instance layer_set_visible layer_get_visible layer_exists layer_x layer_y layer_get_x layer_get_y layer_hspeed layer_vspeed layer_get_hspeed layer_get_vspeed layer_script_begin layer_script_end layer_shader layer_get_script_begin layer_get_script_end layer_get_shader layer_set_target_room layer_get_target_room layer_reset_target_room layer_get_all layer_get_all_elements layer_get_name layer_depth layer_get_element_layer layer_get_element_type layer_element_move layer_force_draw_depth layer_is_draw_depth_forced layer_get_forced_depth layer_background_get_id layer_background_exists layer_background_create layer_background_destroy layer_background_visible layer_background_change layer_background_sprite layer_background_htiled layer_background_vtiled layer_background_stretch layer_background_yscale layer_background_xscale layer_background_blend layer_background_alpha layer_background_index layer_background_speed layer_background_get_visible layer_background_get_sprite layer_background_get_htiled layer_background_get_vtiled layer_background_get_stretch layer_background_get_yscale layer_background_get_xscale layer_background_get_blend layer_background_get_alpha layer_background_get_index layer_background_get_speed layer_sprite_get_id layer_sprite_exists layer_sprite_create layer_sprite_destroy layer_sprite_change layer_sprite_index layer_sprite_speed layer_sprite_xscale layer_sprite_yscale layer_sprite_angle layer_sprite_blend layer_sprite_alpha layer_sprite_x layer_sprite_y layer_sprite_get_sprite layer_sprite_get_index layer_sprite_get_speed layer_sprite_get_xscale layer_sprite_get_yscale layer_sprite_get_angle layer_sprite_get_blend layer_sprite_get_alpha layer_sprite_get_x layer_sprite_get_y layer_tilemap_get_id layer_tilemap_exists layer_tilemap_create layer_tilemap_destroy tilemap_tileset tilemap_x tilemap_y tilemap_set tilemap_set_at_pixel tilemap_get_tileset tilemap_get_tile_width tilemap_get_tile_height tilemap_get_width tilemap_get_height tilemap_get_x tilemap_get_y tilemap_get tilemap_get_at_pixel tilemap_get_cell_x_at_pixel tilemap_get_cell_y_at_pixel tilemap_clear draw_tilemap draw_tile tilemap_set_global_mask tilemap_get_global_mask tilemap_set_mask tilemap_get_mask tilemap_get_frame tile_set_empty tile_set_index tile_set_flip tile_set_mirror tile_set_rotate tile_get_empty tile_get_index tile_get_flip tile_get_mirror tile_get_rotate layer_tile_exists layer_tile_create layer_tile_destroy layer_tile_change layer_tile_xscale layer_tile_yscale layer_tile_blend layer_tile_alpha layer_tile_x layer_tile_y layer_tile_region layer_tile_visible layer_tile_get_sprite layer_tile_get_xscale layer_tile_get_yscale layer_tile_get_blend layer_tile_get_alpha layer_tile_get_x layer_tile_get_y layer_tile_get_region layer_tile_get_visible layer_instance_get_instance instance_activate_layer instance_deactivate_layer camera_create camera_create_view camera_destroy camera_apply camera_get_active camera_get_default camera_set_default camera_set_view_mat camera_set_proj_mat camera_set_update_script camera_set_begin_script camera_set_end_script camera_set_view_pos camera_set_view_size camera_set_view_speed camera_set_view_border camera_set_view_angle camera_set_view_target camera_get_view_mat camera_get_proj_mat camera_get_update_script camera_get_begin_script camera_get_end_script camera_get_view_x camera_get_view_y camera_get_view_width camera_get_view_height camera_get_view_speed_x camera_get_view_speed_y camera_get_view_border_x camera_get_view_border_y camera_get_view_angle camera_get_view_target view_get_camera view_get_visible view_get_xport view_get_yport view_get_wport view_get_hport view_get_surface_id view_set_camera view_set_visible view_set_xport view_set_yport view_set_wport view_set_hport view_set_surface_id gesture_drag_time gesture_drag_distance gesture_flick_speed gesture_double_tap_time gesture_double_tap_distance gesture_pinch_distance gesture_pinch_angle_towards gesture_pinch_angle_away gesture_rotate_time gesture_rotate_angle gesture_tap_count gesture_get_drag_time gesture_get_drag_distance gesture_get_flick_speed gesture_get_double_tap_time gesture_get_double_tap_distance gesture_get_pinch_distance gesture_get_pinch_angle_towards gesture_get_pinch_angle_away gesture_get_rotate_time gesture_get_rotate_angle gesture_get_tap_count keyboard_virtual_show keyboard_virtual_hide keyboard_virtual_status keyboard_virtual_height",
+literal:"self other all noone global local undefined pointer_invalid pointer_null path_action_stop path_action_restart path_action_continue path_action_reverse true false pi GM_build_date GM_version GM_runtime_version  timezone_local timezone_utc gamespeed_fps gamespeed_microseconds  ev_create ev_destroy ev_step ev_alarm ev_keyboard ev_mouse ev_collision ev_other ev_draw ev_draw_begin ev_draw_end ev_draw_pre ev_draw_post ev_keypress ev_keyrelease ev_trigger ev_left_button ev_right_button ev_middle_button ev_no_button ev_left_press ev_right_press ev_middle_press ev_left_release ev_right_release ev_middle_release ev_mouse_enter ev_mouse_leave ev_mouse_wheel_up ev_mouse_wheel_down ev_global_left_button ev_global_right_button ev_global_middle_button ev_global_left_press ev_global_right_press ev_global_middle_press ev_global_left_release ev_global_right_release ev_global_middle_release ev_joystick1_left ev_joystick1_right ev_joystick1_up ev_joystick1_down ev_joystick1_button1 ev_joystick1_button2 ev_joystick1_button3 ev_joystick1_button4 ev_joystick1_button5 ev_joystick1_button6 ev_joystick1_button7 ev_joystick1_button8 ev_joystick2_left ev_joystick2_right ev_joystick2_up ev_joystick2_down ev_joystick2_button1 ev_joystick2_button2 ev_joystick2_button3 ev_joystick2_button4 ev_joystick2_button5 ev_joystick2_button6 ev_joystick2_button7 ev_joystick2_button8 ev_outside ev_boundary ev_game_start ev_game_end ev_room_start ev_room_end ev_no_more_lives ev_animation_end ev_end_of_path ev_no_more_health ev_close_button ev_user0 ev_user1 ev_user2 ev_user3 ev_user4 ev_user5 ev_user6 ev_user7 ev_user8 ev_user9 ev_user10 ev_user11 ev_user12 ev_user13 ev_user14 ev_user15 ev_step_normal ev_step_begin ev_step_end ev_gui ev_gui_begin ev_gui_end ev_cleanup ev_gesture ev_gesture_tap ev_gesture_double_tap ev_gesture_drag_start ev_gesture_dragging ev_gesture_drag_end ev_gesture_flick ev_gesture_pinch_start ev_gesture_pinch_in ev_gesture_pinch_out ev_gesture_pinch_end ev_gesture_rotate_start ev_gesture_rotating ev_gesture_rotate_end ev_global_gesture_tap ev_global_gesture_double_tap ev_global_gesture_drag_start ev_global_gesture_dragging ev_global_gesture_drag_end ev_global_gesture_flick ev_global_gesture_pinch_start ev_global_gesture_pinch_in ev_global_gesture_pinch_out ev_global_gesture_pinch_end ev_global_gesture_rotate_start ev_global_gesture_rotating ev_global_gesture_rotate_end vk_nokey vk_anykey vk_enter vk_return vk_shift vk_control vk_alt vk_escape vk_space vk_backspace vk_tab vk_pause vk_printscreen vk_left vk_right vk_up vk_down vk_home vk_end vk_delete vk_insert vk_pageup vk_pagedown vk_f1 vk_f2 vk_f3 vk_f4 vk_f5 vk_f6 vk_f7 vk_f8 vk_f9 vk_f10 vk_f11 vk_f12 vk_numpad0 vk_numpad1 vk_numpad2 vk_numpad3 vk_numpad4 vk_numpad5 vk_numpad6 vk_numpad7 vk_numpad8 vk_numpad9 vk_divide vk_multiply vk_subtract vk_add vk_decimal vk_lshift vk_lcontrol vk_lalt vk_rshift vk_rcontrol vk_ralt  mb_any mb_none mb_left mb_right mb_middle c_aqua c_black c_blue c_dkgray c_fuchsia c_gray c_green c_lime c_ltgray c_maroon c_navy c_olive c_purple c_red c_silver c_teal c_white c_yellow c_orange fa_left fa_center fa_right fa_top fa_middle fa_bottom pr_pointlist pr_linelist pr_linestrip pr_trianglelist pr_trianglestrip pr_trianglefan bm_complex bm_normal bm_add bm_max bm_subtract bm_zero bm_one bm_src_colour bm_inv_src_colour bm_src_color bm_inv_src_color bm_src_alpha bm_inv_src_alpha bm_dest_alpha bm_inv_dest_alpha bm_dest_colour bm_inv_dest_colour bm_dest_color bm_inv_dest_color bm_src_alpha_sat tf_point tf_linear tf_anisotropic mip_off mip_on mip_markedonly audio_falloff_none audio_falloff_inverse_distance audio_falloff_inverse_distance_clamped audio_falloff_linear_distance audio_falloff_linear_distance_clamped audio_falloff_exponent_distance audio_falloff_exponent_distance_clamped audio_old_system audio_new_system audio_mono audio_stereo audio_3d cr_default cr_none cr_arrow cr_cross cr_beam cr_size_nesw cr_size_ns cr_size_nwse cr_size_we cr_uparrow cr_hourglass cr_drag cr_appstart cr_handpoint cr_size_all spritespeed_framespersecond spritespeed_framespergameframe asset_object asset_unknown asset_sprite asset_sound asset_room asset_path asset_script asset_font asset_timeline asset_tiles asset_shader fa_readonly fa_hidden fa_sysfile fa_volumeid fa_directory fa_archive  ds_type_map ds_type_list ds_type_stack ds_type_queue ds_type_grid ds_type_priority ef_explosion ef_ring ef_ellipse ef_firework ef_smoke ef_smokeup ef_star ef_spark ef_flare ef_cloud ef_rain ef_snow pt_shape_pixel pt_shape_disk pt_shape_square pt_shape_line pt_shape_star pt_shape_circle pt_shape_ring pt_shape_sphere pt_shape_flare pt_shape_spark pt_shape_explosion pt_shape_cloud pt_shape_smoke pt_shape_snow ps_distr_linear ps_distr_gaussian ps_distr_invgaussian ps_shape_rectangle ps_shape_ellipse ps_shape_diamond ps_shape_line ty_real ty_string dll_cdecl dll_stdcall matrix_view matrix_projection matrix_world os_win32 os_windows os_macosx os_ios os_android os_symbian os_linux os_unknown os_winphone os_tizen os_win8native os_wiiu os_3ds  os_psvita os_bb10 os_ps4 os_xboxone os_ps3 os_xbox360 os_uwp os_tvos os_switch browser_not_a_browser browser_unknown browser_ie browser_firefox browser_chrome browser_safari browser_safari_mobile browser_opera browser_tizen browser_edge browser_windows_store browser_ie_mobile  device_ios_unknown device_ios_iphone device_ios_iphone_retina device_ios_ipad device_ios_ipad_retina device_ios_iphone5 device_ios_iphone6 device_ios_iphone6plus device_emulator device_tablet display_landscape display_landscape_flipped display_portrait display_portrait_flipped tm_sleep tm_countvsyncs of_challenge_win of_challen ge_lose of_challenge_tie leaderboard_type_number leaderboard_type_time_mins_secs cmpfunc_never cmpfunc_less cmpfunc_equal cmpfunc_lessequal cmpfunc_greater cmpfunc_notequal cmpfunc_greaterequal cmpfunc_always cull_noculling cull_clockwise cull_counterclockwise lighttype_dir lighttype_point iap_ev_storeload iap_ev_product iap_ev_purchase iap_ev_consume iap_ev_restore iap_storeload_ok iap_storeload_failed iap_status_uninitialised iap_status_unavailable iap_status_loading iap_status_available iap_status_processing iap_status_restoring iap_failed iap_unavailable iap_available iap_purchased iap_canceled iap_refunded fb_login_default fb_login_fallback_to_webview fb_login_no_fallback_to_webview fb_login_forcing_webview fb_login_use_system_account fb_login_forcing_safari  phy_joint_anchor_1_x phy_joint_anchor_1_y phy_joint_anchor_2_x phy_joint_anchor_2_y phy_joint_reaction_force_x phy_joint_reaction_force_y phy_joint_reaction_torque phy_joint_motor_speed phy_joint_angle phy_joint_motor_torque phy_joint_max_motor_torque phy_joint_translation phy_joint_speed phy_joint_motor_force phy_joint_max_motor_force phy_joint_length_1 phy_joint_length_2 phy_joint_damping_ratio phy_joint_frequency phy_joint_lower_angle_limit phy_joint_upper_angle_limit phy_joint_angle_limits phy_joint_max_length phy_joint_max_torque phy_joint_max_force phy_debug_render_aabb phy_debug_render_collision_pairs phy_debug_render_coms phy_debug_render_core_shapes phy_debug_render_joints phy_debug_render_obb phy_debug_render_shapes  phy_particle_flag_water phy_particle_flag_zombie phy_particle_flag_wall phy_particle_flag_spring phy_particle_flag_elastic phy_particle_flag_viscous phy_particle_flag_powder phy_particle_flag_tensile phy_particle_flag_colourmixing phy_particle_flag_colormixing phy_particle_group_flag_solid phy_particle_group_flag_rigid phy_particle_data_flag_typeflags phy_particle_data_flag_position phy_particle_data_flag_velocity phy_particle_data_flag_colour phy_particle_data_flag_color phy_particle_data_flag_category  achievement_our_info achievement_friends_info achievement_leaderboard_info achievement_achievement_info achievement_filter_all_players achievement_filter_friends_only achievement_filter_favorites_only achievement_type_achievement_challenge achievement_type_score_challenge achievement_pic_loaded  achievement_show_ui achievement_show_profile achievement_show_leaderboard achievement_show_achievement achievement_show_bank achievement_show_friend_picker achievement_show_purchase_prompt network_socket_tcp network_socket_udp network_socket_bluetooth network_type_connect network_type_disconnect network_type_data network_type_non_blocking_connect network_config_connect_timeout network_config_use_non_blocking_socket network_config_enable_reliable_udp network_config_disable_reliable_udp buffer_fixed buffer_grow buffer_wrap buffer_fast buffer_vbuffer buffer_network buffer_u8 buffer_s8 buffer_u16 buffer_s16 buffer_u32 buffer_s32 buffer_u64 buffer_f16 buffer_f32 buffer_f64 buffer_bool buffer_text buffer_string buffer_surface_copy buffer_seek_start buffer_seek_relative buffer_seek_end buffer_generalerror buffer_outofspace buffer_outofbounds buffer_invalidtype  text_type button_type input_type ANSI_CHARSET DEFAULT_CHARSET EASTEUROPE_CHARSET RUSSIAN_CHARSET SYMBOL_CHARSET SHIFTJIS_CHARSET HANGEUL_CHARSET GB2312_CHARSET CHINESEBIG5_CHARSET JOHAB_CHARSET HEBREW_CHARSET ARABIC_CHARSET GREEK_CHARSET TURKISH_CHARSET VIETNAMESE_CHARSET THAI_CHARSET MAC_CHARSET BALTIC_CHARSET OEM_CHARSET  gp_face1 gp_face2 gp_face3 gp_face4 gp_shoulderl gp_shoulderr gp_shoulderlb gp_shoulderrb gp_select gp_start gp_stickl gp_stickr gp_padu gp_padd gp_padl gp_padr gp_axislh gp_axislv gp_axisrh gp_axisrv ov_friends ov_community ov_players ov_settings ov_gamegroup ov_achievements lb_sort_none lb_sort_ascending lb_sort_descending lb_disp_none lb_disp_numeric lb_disp_time_sec lb_disp_time_ms ugc_result_success ugc_filetype_community ugc_filetype_microtrans ugc_visibility_public ugc_visibility_friends_only ugc_visibility_private ugc_query_RankedByVote ugc_query_RankedByPublicationDate ugc_query_AcceptedForGameRankedByAcceptanceDate ugc_query_RankedByTrend ugc_query_FavoritedByFriendsRankedByPublicationDate ugc_query_CreatedByFriendsRankedByPublicationDate ugc_query_RankedByNumTimesReported ugc_query_CreatedByFollowedUsersRankedByPublicationDate ugc_query_NotYetRated ugc_query_RankedByTotalVotesAsc ugc_query_RankedByVotesUp ugc_query_RankedByTextSearch ugc_sortorder_CreationOrderDesc ugc_sortorder_CreationOrderAsc ugc_sortorder_TitleAsc ugc_sortorder_LastUpdatedDesc ugc_sortorder_SubscriptionDateDesc ugc_sortorder_VoteScoreDesc ugc_sortorder_ForModeration ugc_list_Published ugc_list_VotedOn ugc_list_VotedUp ugc_list_VotedDown ugc_list_WillVoteLater ugc_list_Favorited ugc_list_Subscribed ugc_list_UsedOrPlayed ugc_list_Followed ugc_match_Items ugc_match_Items_Mtx ugc_match_Items_ReadyToUse ugc_match_Collections ugc_match_Artwork ugc_match_Videos ugc_match_Screenshots ugc_match_AllGuides ugc_match_WebGuides ugc_match_IntegratedGuides ugc_match_UsableInGame ugc_match_ControllerBindings  vertex_usage_position vertex_usage_colour vertex_usage_color vertex_usage_normal vertex_usage_texcoord vertex_usage_textcoord vertex_usage_blendweight vertex_usage_blendindices vertex_usage_psize vertex_usage_tangent vertex_usage_binormal vertex_usage_fog vertex_usage_depth vertex_usage_sample vertex_type_float1 vertex_type_float2 vertex_type_float3 vertex_type_float4 vertex_type_colour vertex_type_color vertex_type_ubyte4 layerelementtype_undefined layerelementtype_background layerelementtype_instance layerelementtype_oldtilemap layerelementtype_sprite layerelementtype_tilemap layerelementtype_particlesystem layerelementtype_tile tile_rotate tile_flip tile_mirror tile_index_mask kbv_type_default kbv_type_ascii kbv_type_url kbv_type_email kbv_type_numbers kbv_type_phone kbv_type_phone_name kbv_returnkey_default kbv_returnkey_go kbv_returnkey_google kbv_returnkey_join kbv_returnkey_next kbv_returnkey_route kbv_returnkey_search kbv_returnkey_send kbv_returnkey_yahoo kbv_returnkey_done kbv_returnkey_continue kbv_returnkey_emergency kbv_autocapitalize_none kbv_autocapitalize_words kbv_autocapitalize_sentences kbv_autocapitalize_characters",
+symbol:"argument_relative argument argument0 argument1 argument2 argument3 argument4 argument5 argument6 argument7 argument8 argument9 argument10 argument11 argument12 argument13 argument14 argument15 argument_count x y xprevious yprevious xstart ystart hspeed vspeed direction speed friction gravity gravity_direction path_index path_position path_positionprevious path_speed path_scale path_orientation path_endaction object_index id solid persistent mask_index instance_count instance_id room_speed fps fps_real current_time current_year current_month current_day current_weekday current_hour current_minute current_second alarm timeline_index timeline_position timeline_speed timeline_running timeline_loop room room_first room_last room_width room_height room_caption room_persistent score lives health show_score show_lives show_health caption_score caption_lives caption_health event_type event_number event_object event_action application_surface gamemaker_pro gamemaker_registered gamemaker_version error_occurred error_last debug_mode keyboard_key keyboard_lastkey keyboard_lastchar keyboard_string mouse_x mouse_y mouse_button mouse_lastbutton cursor_sprite visible sprite_index sprite_width sprite_height sprite_xoffset sprite_yoffset image_number image_index image_speed depth image_xscale image_yscale image_angle image_alpha image_blend bbox_left bbox_right bbox_top bbox_bottom layer background_colour  background_showcolour background_color background_showcolor view_enabled view_current view_visible view_xview view_yview view_wview view_hview view_xport view_yport view_wport view_hport view_angle view_hborder view_vborder view_hspeed view_vspeed view_object view_surface_id view_camera game_id game_display_name game_project_name game_save_id working_directory temp_directory program_directory browser_width browser_height os_type os_device os_browser os_version display_aa async_load delta_time webgl_enabled event_data iap_data phy_rotation phy_position_x phy_position_y phy_angular_velocity phy_linear_velocity_x phy_linear_velocity_y phy_speed_x phy_speed_y phy_speed phy_angular_damping phy_linear_damping phy_bullet phy_fixed_rotation phy_active phy_mass phy_inertia phy_com_x phy_com_y phy_dynamic phy_kinematic phy_sleeping phy_collision_points phy_collision_x phy_collision_y phy_col_normal_x phy_col_normal_y phy_position_xprevious phy_position_yprevious"},
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("go",function(a){var c={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};
+return{aliases:["golang"],keywords:c,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0,contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:c,illegal:/["']/}]}]}});b.registerLanguage("golo",function(a){return{keywords:{keyword:"println readln print import module function local return let var while for foreach times in case when match with break continue augment augmentation each find filter reduce if then else otherwise try catch finally raise throw orIfNull DynamicObject|10 DynamicVariable struct Observable map set vector list array",
+literal:"true false null"},contains:[a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("gradle",function(a){return{case_insensitive:!0,keywords:{keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"},
 contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.REGEXP_MODE]}});b.registerLanguage("groovy",function(a){return{keywords:{literal:"true false null",keyword:"byte short char int long boolean float double void def as in assert trait super this abstract static volatile transient public private protected synchronized final class interface enum if else for while switch case break default continue throw throws try catch finally implements extends new import package return instanceof"},
 contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",begin:'"""',end:'"""'},{className:"string",begin:"'''",end:"'''"},{className:"string",begin:"\\$/",end:"/\\$",relevance:10},a.APOS_STRING_MODE,{className:"regexp",begin:/~?\/[^\/\n]+\//,contains:[a.BACKSLASH_ESCAPE]},a.QUOTE_STRING_MODE,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},a.BINARY_NUMBER_MODE,
 {className:"class",beginKeywords:"class interface trait enum",end:"{",illegal:":",contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{className:"string",begin:/[^\?]{0}[A-Za-z0-9_$]+ *:/},{begin:/\?/,end:/:/},{className:"symbol",begin:"^\\s*[A-Za-z0-9_$]+:",relevance:0}],illegal:/#|<\//}});b.registerLanguage("haml",function(a){return{case_insensitive:!0,contains:[{className:"meta",begin:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",
@@ -196,27 +209,33 @@
 excludeBegin:!0,excludeEnd:!0},a.TITLE_MODE],keywords:{keyword:"abstract from to"}},{className:"class",begin:"\\b(class|interface) +",end:"[\\{$]",excludeEnd:!0,keywords:"class interface",contains:[{className:"keyword",begin:"\\b(extends|implements) +",keywords:"extends implements",contains:[{className:"type",begin:a.IDENT_RE,relevance:0}]},a.TITLE_MODE]},{className:"function",beginKeywords:"function",end:"\\(",excludeEnd:!0,illegal:"\\S",contains:[a.TITLE_MODE]}],illegal:/<\//}});b.registerLanguage("hsp",
 function(a){return{case_insensitive:!0,lexemes:/[\w\._]+/,keywords:"goto gosub return break repeat loop continue wait await dim sdim foreach dimtype dup dupptr end stop newmod delmod mref run exgoto on mcall assert logmes newlab resume yield onexit onerror onkey onclick oncmd exist delete mkdir chdir dirlist bload bsave bcopy memfile if else poke wpoke lpoke getstr chdpm memexpand memcpy memset notesel noteadd notedel noteload notesave randomize noteunsel noteget split strrep setease button chgdisp exec dialog mmload mmplay mmstop mci pset pget syscolor mes print title pos circle cls font sysfont objsize picload color palcolor palette redraw width gsel gcopy gzoom gmode bmpsave hsvcolor getkey listbox chkbox combox input mesbox buffer screen bgscr mouse objsel groll line clrobj boxf objprm objmode stick grect grotate gsquare gradf objimage objskip objenable celload celdiv celput newcom querycom delcom cnvstow comres axobj winobj sendmsg comevent comevarg sarrayconv callfunc cnvwtos comevdisp libptr system hspstat hspver stat cnt err strsize looplev sublev iparam wparam lparam refstr refdval int rnd strlen length length2 length3 length4 vartype gettime peek wpeek lpeek varptr varuse noteinfo instr abs limit getease str strmid strf getpath strtrim sin cos tan atan sqrt double absf expf logf limitf powf geteasef mousex mousey mousew hwnd hinstance hdc ginfo objinfo dirinfo sysinfo thismod __hspver__ __hsp30__ __date__ __time__ __line__ __file__ _debug __hspdef__ and or xor not screen_normal screen_palette screen_hide screen_fixedsize screen_tool screen_frame gmode_gdi gmode_mem gmode_rgb0 gmode_alpha gmode_rgb0alpha gmode_add gmode_sub gmode_pixela ginfo_mx ginfo_my ginfo_act ginfo_sel ginfo_wx1 ginfo_wy1 ginfo_wx2 ginfo_wy2 ginfo_vx ginfo_vy ginfo_sizex ginfo_sizey ginfo_winx ginfo_winy ginfo_mesx ginfo_mesy ginfo_r ginfo_g ginfo_b ginfo_paluse ginfo_dispx ginfo_dispy ginfo_cx ginfo_cy ginfo_intid ginfo_newid ginfo_sx ginfo_sy objinfo_mode objinfo_bmscr objinfo_hwnd notemax notesize dir_cur dir_exe dir_win dir_sys dir_cmdline dir_desktop dir_mydoc dir_tv font_normal font_bold font_italic font_underline font_strikeout font_antialias objmode_normal objmode_guifont objmode_usefont gsquare_grad msgothic msmincho do until while wend for next _break _continue switch case default swbreak swend ddim ldim alloc m_pi rad2deg deg2rad ease_linear ease_quad_in ease_quad_out ease_quad_inout ease_cubic_in ease_cubic_out ease_cubic_inout ease_quartic_in ease_quartic_out ease_quartic_inout ease_bounce_in ease_bounce_out ease_bounce_inout ease_shake_in ease_shake_out ease_shake_inout ease_loop",
 contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",begin:'{"',end:'"}',contains:[a.BACKSLASH_ESCAPE]},a.COMMENT(";","$",{relevance:0}),{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"addion cfunc cmd cmpopt comfunc const defcfunc deffunc define else endif enum epack func global if ifdef ifndef include modcfunc modfunc modinit modterm module pack packopt regcmd runtime undef usecom uselib"},contains:[a.inherit(a.QUOTE_STRING_MODE,
-{className:"meta-string"}),a.NUMBER_MODE,a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"symbol",begin:"^\\*(\\w+|@)"},a.NUMBER_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("htmlbars",function(a){var b={endsWithParent:!0,relevance:0,keywords:{keyword:"as",built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE,{illegal:/\}\}/,
+{className:"meta-string"}),a.NUMBER_MODE,a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"symbol",begin:"^\\*(\\w+|@)"},a.NUMBER_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("htmlbars",function(a){var c={endsWithParent:!0,relevance:0,keywords:{keyword:"as",built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE,{illegal:/\}\}/,
 begin:/[a-zA-Z0-9_]+=/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/[a-zA-Z0-9_]+/}]},a.NUMBER_MODE]};return{case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT("{{!(--)?","(--)?}}"),{className:"template-tag",begin:/\{\{[#\/]/,end:/\}\}/,contains:[{className:"name",begin:/[a-zA-Z\.\-]+/,keywords:{"builtin-name":"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},
-starts:b}]},{className:"template-variable",begin:/\{\{[a-zA-Z][a-zA-Z\-]+/,end:/\}\}/,keywords:{keyword:"as",built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE]}]}});b.registerLanguage("http",function(a){return{aliases:["https"],illegal:"\\S",contains:[{begin:"^HTTP/[0-9\\.]+",end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},
-{begin:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:"HTTP/[0-9\\.]+"},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}});b.registerLanguage("hy",function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},d=a.inherit(a.QUOTE_STRING_MODE,
-{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),f={className:"literal",begin:/\b([Tt]rue|[Ff]alse|nil|None)\b/},g={begin:"[\\[\\{]",end:"[\\]\\}]"},l={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},k=a.COMMENT("\\^\\{","\\}"),m={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},q={keywords:{"builtin-name":"!= % %= & &= * ** **= *= *map + += , --build-class-- --import-- -= . / // //= /= < << <<= <= = > >= >> >>= @ @= ^ ^= abs accumulate all and any ap-compose ap-dotimes ap-each ap-each-while ap-filter ap-first ap-if ap-last ap-map ap-map-when ap-pipe ap-reduce ap-reject apply as-> ascii assert assoc bin break butlast callable calling-module-name car case cdr chain chr coll? combinations compile compress cond cons cons? continue count curry cut cycle dec def default-method defclass defmacro defmacro-alias defmacro/g! defmain defmethod defmulti defn defn-alias defnc defnr defreader defseq del delattr delete-route dict-comp dir disassemble dispatch-reader-macro distinct divmod do doto drop drop-last drop-while empty? end-sequence eval eval-and-compile eval-when-compile even? every? except exec filter first flatten float? fn fnc fnr for for* format fraction genexpr gensym get getattr global globals group-by hasattr hash hex id identity if if* if-not if-python2 import in inc input instance? integer integer-char? integer? interleave interpose is is-coll is-cons is-empty is-even is-every is-float is-instance is-integer is-integer-char is-iterable is-iterator is-keyword is-neg is-none is-not is-numeric is-odd is-pos is-string is-symbol is-zero isinstance islice issubclass iter iterable? iterate iterator? keyword keyword? lambda last len let lif lif-not list* list-comp locals loop macro-error macroexpand macroexpand-1 macroexpand-all map max merge-with method-decorator min multi-decorator multicombinations name neg? next none? nonlocal not not-in not? nth numeric? oct odd? open or ord partition permutations pos? post-route postwalk pow prewalk print product profile/calls profile/cpu put-route quasiquote quote raise range read read-str recursive-replace reduce remove repeat repeatedly repr require rest round route route-with-methods rwm second seq set-comp setattr setv some sorted string string? sum switch symbol? take take-nth take-while tee try unless unquote unquote-splicing vars walk when while with with* with-decorator with-gensyms xi xor yield yield-from zero? zip zip-longest | |= ~"},
-lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},r=[n,d,l,k,e,m,g,b,f,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),q,h];h.contains=r;g.contains=r;return{aliases:["hylang"],illegal:/\S/,contains:[{className:"meta",begin:"^#!",end:"$"},n,d,l,k,e,m,g,b,f]}});b.registerLanguage("inform7",function(a){return{aliases:["i7"],case_insensitive:!0,
+starts:c}]},{className:"template-variable",begin:/\{\{[a-zA-Z][a-zA-Z\-]+/,end:/\}\}/,keywords:{keyword:"as",built_in:"action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view"},contains:[a.QUOTE_STRING_MODE]}]}});b.registerLanguage("http",function(a){return{aliases:["https"],illegal:"\\S",contains:[{begin:"^HTTP/[0-9\\.]+",end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},
+{begin:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:"HTTP/[0-9\\.]+"},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}});b.registerLanguage("hy",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,
+{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),f={className:"literal",begin:/\b([Tt]rue|[Ff]alse|nil|None)\b/},g={begin:"[\\[\\{]",end:"[\\]\\}]"},k={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},m=a.COMMENT("\\^\\{","\\}"),p={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},l={keywords:{"builtin-name":"!= % %= & &= * ** **= *= *map + += , --build-class-- --import-- -= . / // //= /= < << <<= <= = > >= >> >>= @ @= ^ ^= abs accumulate all and any ap-compose ap-dotimes ap-each ap-each-while ap-filter ap-first ap-if ap-last ap-map ap-map-when ap-pipe ap-reduce ap-reject apply as-> ascii assert assoc bin break butlast callable calling-module-name car case cdr chain chr coll? combinations compile compress cond cons cons? continue count curry cut cycle dec def default-method defclass defmacro defmacro-alias defmacro/g! defmain defmethod defmulti defn defn-alias defnc defnr defreader defseq del delattr delete-route dict-comp dir disassemble dispatch-reader-macro distinct divmod do doto drop drop-last drop-while empty? end-sequence eval eval-and-compile eval-when-compile even? every? except exec filter first flatten float? fn fnc fnr for for* format fraction genexpr gensym get getattr global globals group-by hasattr hash hex id identity if if* if-not if-python2 import in inc input instance? integer integer-char? integer? interleave interpose is is-coll is-cons is-empty is-even is-every is-float is-instance is-integer is-integer-char is-iterable is-iterator is-keyword is-neg is-none is-not is-numeric is-odd is-pos is-string is-symbol is-zero isinstance islice issubclass iter iterable? iterate iterator? keyword keyword? lambda last len let lif lif-not list* list-comp locals loop macro-error macroexpand macroexpand-1 macroexpand-all map max merge-with method-decorator min multi-decorator multicombinations name neg? next none? nonlocal not not-in not? nth numeric? oct odd? open or ord partition permutations pos? post-route postwalk pow prewalk print product profile/calls profile/cpu put-route quasiquote quote raise range read read-str recursive-replace reduce remove repeat repeatedly repr require rest round route route-with-methods rwm second seq set-comp setattr setv some sorted string string? sum switch symbol? take take-nth take-while tee try unless unquote unquote-splicing vars walk when while with with* with-decorator with-gensyms xi xor yield yield-from zero? zip zip-longest | |= ~"},
+lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},q=[n,b,k,m,e,p,g,c,f,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),l,h];h.contains=q;g.contains=q;return{aliases:["hylang"],illegal:/\S/,contains:[{className:"meta",begin:"^#!",end:"$"},n,b,k,m,e,p,g,c,f]}});b.registerLanguage("inform7",function(a){return{aliases:["i7"],case_insensitive:!0,
 keywords:{keyword:"thing room person man woman animal container supporter backdrop door scenery open closed locked inside gender is are say understand kind of rule"},contains:[{className:"string",begin:'"',end:'"',relevance:0,contains:[{className:"subst",begin:"\\[",end:"\\]"}]},{className:"section",begin:/^(Volume|Book|Part|Chapter|Section|Table)\b/,end:"$"},{begin:/^(Check|Carry out|Report|Instead of|To|Rule|When|Before|After)\b/,end:":",contains:[{begin:"\\(This",end:"\\)"}]},{className:"comment",
-begin:"\\[",end:"\\]",contains:["self"]}]}});b.registerLanguage("ini",function(a){var b={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]};return{aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[a.COMMENT(";","$"),a.HASH_COMMENT_MODE,{className:"section",begin:/^\s*\[+/,end:/\]+/},{begin:/^[a-z0-9\[\]_-]+\s*=\s*/,end:"$",returnBegin:!0,contains:[{className:"attr",
-begin:/[a-z0-9\[\]_-]+/},{begin:/=/,endsWithParent:!0,relevance:0,contains:[{className:"literal",begin:/\bon|off|true|false|yes|no\b/},{className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},b,{className:"number",begin:/([\+\-]+)?[\d]+_[\d_]+/},a.NUMBER_MODE]}]}]}});b.registerLanguage("irpf90",function(a){return{case_insensitive:!0,keywords:{literal:".False. .True.",keyword:"kind do while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure integer real character complex logical dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data begin_provider &begin_provider end_provider begin_shell end_shell begin_template end_template subst assert touch soft_touch provide no_dep free irp_if irp_else irp_endif irp_write irp_read",
+begin:"\\[",end:"\\]",contains:["self"]}]}});b.registerLanguage("ini",function(a){var c={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]};return{aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[a.COMMENT(";","$"),a.HASH_COMMENT_MODE,{className:"section",begin:/^\s*\[+/,end:/\]+/},{begin:/^[a-z0-9\[\]_-]+\s*=\s*/,end:"$",returnBegin:!0,contains:[{className:"attr",
+begin:/[a-z0-9\[\]_-]+/},{begin:/=/,endsWithParent:!0,relevance:0,contains:[{className:"literal",begin:/\bon|off|true|false|yes|no\b/},{className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},c,{className:"number",begin:/([\+\-]+)?[\d]+_[\d_]+/},a.NUMBER_MODE]}]}]}});b.registerLanguage("irpf90",function(a){return{case_insensitive:!0,keywords:{literal:".False. .True.",keyword:"kind do while private call intrinsic where elsewhere type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. goto save else use module select case access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit continue format pause cycle exit c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg synchronous nopass non_overridable pass protected volatile abstract extends import non_intrinsic value deferred generic final enumerator class associate bind enum c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated  c_f_pointer c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure integer real character complex logical dimension allocatable|10 parameter external implicit|10 none double precision assign intent optional pointer target in out common equivalence data begin_provider &begin_provider end_provider begin_shell end_shell begin_template end_template subst assert touch soft_touch provide no_dep free irp_if irp_else irp_endif irp_write irp_read",
 built_in:"alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_ofacosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr num_images parity popcnt poppar shifta shiftl shiftr this_image IRP_ALIGN irp_here"},
 illegal:/\/\*/,contains:[a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{className:"string",relevance:0}),{className:"function",beginKeywords:"subroutine function program",illegal:"[${=\\n]",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},a.COMMENT("!","$",{relevance:0}),a.COMMENT("begin_doc","end_doc",{relevance:10}),{className:"number",begin:"(?=\\b|\\+|\\-|\\.)(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*)(?:[de][+-]?\\d+)?\\b\\.?",
-relevance:0}]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,
-contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+
+relevance:0}]}});b.registerLanguage("isbl",function(a){var c={className:"number",begin:a.NUMBER_RE,relevance:0},b={className:"string",variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]},e={className:"doctag",begin:"\\b(?:TODO|DONE|BEGIN|END|STUB|CHG|FIXME|NOTE|BUG|XXX)\\b",relevance:0};e={variants:[{className:"comment",begin:"//",end:"$",relevance:0,contains:[a.PHRASAL_WORDS_MODE,e]},{className:"comment",begin:"/\\*",end:"\\*/",relevance:0,contains:[a.PHRASAL_WORDS_MODE,e]}]};var f={keyword:"and \u0438 else \u0438\u043d\u0430\u0447\u0435 endexcept endfinally endforeach \u043a\u043e\u043d\u0435\u0446\u0432\u0441\u0435 endif \u043a\u043e\u043d\u0435\u0446\u0435\u0441\u043b\u0438 endwhile \u043a\u043e\u043d\u0435\u0446\u043f\u043e\u043a\u0430 except exitfor finally foreach \u0432\u0441\u0435 if \u0435\u0441\u043b\u0438 in \u0432 not \u043d\u0435 or \u0438\u043b\u0438 try while \u043f\u043e\u043a\u0430 ",
+built_in:"SYSRES_CONST_ACCES_RIGHT_TYPE_EDIT SYSRES_CONST_ACCES_RIGHT_TYPE_FULL SYSRES_CONST_ACCES_RIGHT_TYPE_VIEW SYSRES_CONST_ACCESS_MODE_REQUISITE_CODE SYSRES_CONST_ACCESS_NO_ACCESS_VIEW SYSRES_CONST_ACCESS_NO_ACCESS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW SYSRES_CONST_ACCESS_RIGHTS_VIEW_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_CODE SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_YES_CODE SYSRES_CONST_ACCESS_TYPE_CHANGE SYSRES_CONST_ACCESS_TYPE_CHANGE_CODE SYSRES_CONST_ACCESS_TYPE_EXISTS SYSRES_CONST_ACCESS_TYPE_EXISTS_CODE SYSRES_CONST_ACCESS_TYPE_FULL SYSRES_CONST_ACCESS_TYPE_FULL_CODE SYSRES_CONST_ACCESS_TYPE_VIEW SYSRES_CONST_ACCESS_TYPE_VIEW_CODE SYSRES_CONST_ACTION_TYPE_ABORT SYSRES_CONST_ACTION_TYPE_ACCEPT SYSRES_CONST_ACTION_TYPE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ADD_ATTACHMENT SYSRES_CONST_ACTION_TYPE_CHANGE_CARD SYSRES_CONST_ACTION_TYPE_CHANGE_KIND SYSRES_CONST_ACTION_TYPE_CHANGE_STORAGE SYSRES_CONST_ACTION_TYPE_CONTINUE SYSRES_CONST_ACTION_TYPE_COPY SYSRES_CONST_ACTION_TYPE_CREATE SYSRES_CONST_ACTION_TYPE_CREATE_VERSION SYSRES_CONST_ACTION_TYPE_DELETE SYSRES_CONST_ACTION_TYPE_DELETE_ATTACHMENT SYSRES_CONST_ACTION_TYPE_DELETE_VERSION SYSRES_CONST_ACTION_TYPE_DISABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENABLE_DELEGATE_ACCESS_RIGHTS SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE_AND_PASSWORD SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_PASSWORD SYSRES_CONST_ACTION_TYPE_EXPORT_WITH_LOCK SYSRES_CONST_ACTION_TYPE_EXPORT_WITHOUT_LOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITH_UNLOCK SYSRES_CONST_ACTION_TYPE_IMPORT_WITHOUT_UNLOCK SYSRES_CONST_ACTION_TYPE_LIFE_CYCLE_STAGE SYSRES_CONST_ACTION_TYPE_LOCK SYSRES_CONST_ACTION_TYPE_LOCK_FOR_SERVER SYSRES_CONST_ACTION_TYPE_LOCK_MODIFY SYSRES_CONST_ACTION_TYPE_MARK_AS_READED SYSRES_CONST_ACTION_TYPE_MARK_AS_UNREADED SYSRES_CONST_ACTION_TYPE_MODIFY SYSRES_CONST_ACTION_TYPE_MODIFY_CARD SYSRES_CONST_ACTION_TYPE_MOVE_TO_ARCHIVE SYSRES_CONST_ACTION_TYPE_OFF_ENCRYPTION SYSRES_CONST_ACTION_TYPE_PASSWORD_CHANGE SYSRES_CONST_ACTION_TYPE_PERFORM SYSRES_CONST_ACTION_TYPE_RECOVER_FROM_LOCAL_COPY SYSRES_CONST_ACTION_TYPE_RESTART SYSRES_CONST_ACTION_TYPE_RESTORE_FROM_ARCHIVE SYSRES_CONST_ACTION_TYPE_REVISION SYSRES_CONST_ACTION_TYPE_SEND_BY_MAIL SYSRES_CONST_ACTION_TYPE_SIGN SYSRES_CONST_ACTION_TYPE_START SYSRES_CONST_ACTION_TYPE_UNLOCK SYSRES_CONST_ACTION_TYPE_UNLOCK_FROM_SERVER SYSRES_CONST_ACTION_TYPE_VERSION_STATE SYSRES_CONST_ACTION_TYPE_VERSION_VISIBILITY SYSRES_CONST_ACTION_TYPE_VIEW SYSRES_CONST_ACTION_TYPE_VIEW_SHADOW_COPY SYSRES_CONST_ACTION_TYPE_WORKFLOW_DESCRIPTION_MODIFY SYSRES_CONST_ACTION_TYPE_WRITE_HISTORY SYSRES_CONST_ACTIVE_VERSION_STATE_PICK_VALUE SYSRES_CONST_ADD_REFERENCE_MODE_NAME SYSRES_CONST_ADDITION_REQUISITE_CODE SYSRES_CONST_ADDITIONAL_PARAMS_REQUISITE_CODE SYSRES_CONST_ADITIONAL_JOB_END_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_READ_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_START_DATE_REQUISITE_NAME SYSRES_CONST_ADITIONAL_JOB_STATE_REQUISITE_NAME SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION_CODE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE_ACTION SYSRES_CONST_ALL_ACCEPT_CONDITION_RUS SYSRES_CONST_ALL_USERS_GROUP SYSRES_CONST_ALL_USERS_GROUP_NAME SYSRES_CONST_ALL_USERS_SERVER_GROUP_NAME SYSRES_CONST_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_APP_VIEWER_TYPE_REQUISITE_CODE SYSRES_CONST_APPROVING_SIGNATURE_NAME SYSRES_CONST_APPROVING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE_CODE SYSRES_CONST_ATTACH_TYPE_COMPONENT_TOKEN SYSRES_CONST_ATTACH_TYPE_DOC SYSRES_CONST_ATTACH_TYPE_EDOC SYSRES_CONST_ATTACH_TYPE_FOLDER SYSRES_CONST_ATTACH_TYPE_JOB SYSRES_CONST_ATTACH_TYPE_REFERENCE SYSRES_CONST_ATTACH_TYPE_TASK SYSRES_CONST_AUTH_ENCODED_PASSWORD SYSRES_CONST_AUTH_ENCODED_PASSWORD_CODE SYSRES_CONST_AUTH_NOVELL SYSRES_CONST_AUTH_PASSWORD SYSRES_CONST_AUTH_PASSWORD_CODE SYSRES_CONST_AUTH_WINDOWS SYSRES_CONST_AUTHENTICATING_SIGNATURE_NAME SYSRES_CONST_AUTHENTICATING_SIGNATURE_REQUISITE_CODE SYSRES_CONST_AUTO_ENUM_METHOD_FLAG SYSRES_CONST_AUTO_NUMERATION_CODE SYSRES_CONST_AUTO_STRONG_ENUM_METHOD_FLAG SYSRES_CONST_AUTOTEXT_NAME_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_TEXT_REQUISITE_CODE SYSRES_CONST_AUTOTEXT_USAGE_ALL SYSRES_CONST_AUTOTEXT_USAGE_ALL_CODE SYSRES_CONST_AUTOTEXT_USAGE_SIGN SYSRES_CONST_AUTOTEXT_USAGE_SIGN_CODE SYSRES_CONST_AUTOTEXT_USAGE_WORK SYSRES_CONST_AUTOTEXT_USAGE_WORK_CODE SYSRES_CONST_AUTOTEXT_USE_ANYWHERE_CODE SYSRES_CONST_AUTOTEXT_USE_ON_SIGNING_CODE SYSRES_CONST_AUTOTEXT_USE_ON_WORK_CODE SYSRES_CONST_BEGIN_DATE_REQUISITE_CODE SYSRES_CONST_BLACK_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BLUE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_BTN_PART SYSRES_CONST_CALCULATED_ROLE_TYPE_CODE SYSRES_CONST_CALL_TYPE_VARIABLE_BUTTON_VALUE SYSRES_CONST_CALL_TYPE_VARIABLE_PROGRAM_VALUE SYSRES_CONST_CANCEL_MESSAGE_FUNCTION_RESULT SYSRES_CONST_CARD_PART SYSRES_CONST_CARD_REFERENCE_MODE_NAME SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_AND_ENCRYPT_VALUE SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_VALUE SYSRES_CONST_CHECK_PARAM_VALUE_DATE_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_FLOAT_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_INTEGER_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_PICK_PARAM_TYPE SYSRES_CONST_CHECK_PARAM_VALUE_REEFRENCE_PARAM_TYPE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_CODE_COMPONENT_TYPE_ADMIN SYSRES_CONST_CODE_COMPONENT_TYPE_DEVELOPER SYSRES_CONST_CODE_COMPONENT_TYPE_DOCS SYSRES_CONST_CODE_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_CODE_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_CODE_COMPONENT_TYPE_OTHER SYSRES_CONST_CODE_COMPONENT_TYPE_REFERENCE SYSRES_CONST_CODE_COMPONENT_TYPE_REPORT SYSRES_CONST_CODE_COMPONENT_TYPE_SCRIPT SYSRES_CONST_CODE_COMPONENT_TYPE_URL SYSRES_CONST_CODE_REQUISITE_ACCESS SYSRES_CONST_CODE_REQUISITE_CODE SYSRES_CONST_CODE_REQUISITE_COMPONENT SYSRES_CONST_CODE_REQUISITE_DESCRIPTION SYSRES_CONST_CODE_REQUISITE_EXCLUDE_COMPONENT SYSRES_CONST_CODE_REQUISITE_RECORD SYSRES_CONST_COMMENT_REQ_CODE SYSRES_CONST_COMMON_SETTINGS_REQUISITE_CODE SYSRES_CONST_COMP_CODE_GRD SYSRES_CONST_COMPONENT_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_COMPONENT_TYPE_ADMIN_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DEVELOPER_COMPONENTS SYSRES_CONST_COMPONENT_TYPE_DOCS SYSRES_CONST_COMPONENT_TYPE_EDOC_CARDS SYSRES_CONST_COMPONENT_TYPE_EDOCS SYSRES_CONST_COMPONENT_TYPE_EXTERNAL_EXECUTABLE SYSRES_CONST_COMPONENT_TYPE_OTHER SYSRES_CONST_COMPONENT_TYPE_REFERENCE_TYPES SYSRES_CONST_COMPONENT_TYPE_REFERENCES SYSRES_CONST_COMPONENT_TYPE_REPORTS SYSRES_CONST_COMPONENT_TYPE_SCRIPTS SYSRES_CONST_COMPONENT_TYPE_URL SYSRES_CONST_COMPONENTS_REMOTE_SERVERS_VIEW_CODE SYSRES_CONST_CONDITION_BLOCK_DESCRIPTION SYSRES_CONST_CONST_FIRM_STATUS_COMMON SYSRES_CONST_CONST_FIRM_STATUS_INDIVIDUAL SYSRES_CONST_CONST_NEGATIVE_VALUE SYSRES_CONST_CONST_POSITIVE_VALUE SYSRES_CONST_CONST_SERVER_STATUS_DONT_REPLICATE SYSRES_CONST_CONST_SERVER_STATUS_REPLICATE SYSRES_CONST_CONTENTS_REQUISITE_CODE SYSRES_CONST_DATA_TYPE_BOOLEAN SYSRES_CONST_DATA_TYPE_DATE SYSRES_CONST_DATA_TYPE_FLOAT SYSRES_CONST_DATA_TYPE_INTEGER SYSRES_CONST_DATA_TYPE_PICK SYSRES_CONST_DATA_TYPE_REFERENCE SYSRES_CONST_DATA_TYPE_STRING SYSRES_CONST_DATA_TYPE_TEXT SYSRES_CONST_DATA_TYPE_VARIANT SYSRES_CONST_DATE_CLOSE_REQ_CODE SYSRES_CONST_DATE_FORMAT_DATE_ONLY_CHAR SYSRES_CONST_DATE_OPEN_REQ_CODE SYSRES_CONST_DATE_REQUISITE SYSRES_CONST_DATE_REQUISITE_CODE SYSRES_CONST_DATE_REQUISITE_NAME SYSRES_CONST_DATE_REQUISITE_TYPE SYSRES_CONST_DATE_TYPE_CHAR SYSRES_CONST_DATETIME_FORMAT_VALUE SYSRES_CONST_DEA_ACCESS_RIGHTS_ACTION_CODE SYSRES_CONST_DESCRIPTION_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_DET1_PART SYSRES_CONST_DET2_PART SYSRES_CONST_DET3_PART SYSRES_CONST_DET4_PART SYSRES_CONST_DET5_PART SYSRES_CONST_DET6_PART SYSRES_CONST_DETAIL_DATASET_KEY_REQUISITE_CODE SYSRES_CONST_DETAIL_PICK_REQUISITE_CODE SYSRES_CONST_DETAIL_REQ_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_NAME SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_CODE SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_NAME SYSRES_CONST_DOCUMENT_STORAGES_CODE SYSRES_CONST_DOCUMENT_TEMPLATES_TYPE_NAME SYSRES_CONST_DOUBLE_REQUISITE_CODE SYSRES_CONST_EDITOR_CLOSE_FILE_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_CLOSE_PROCESS_OBSERV_TYPE_CODE SYSRES_CONST_EDITOR_TYPE_REQUISITE_CODE SYSRES_CONST_EDITORS_APPLICATION_NAME_REQUISITE_CODE SYSRES_CONST_EDITORS_CREATE_SEVERAL_PROCESSES_REQUISITE_CODE SYSRES_CONST_EDITORS_EXTENSION_REQUISITE_CODE SYSRES_CONST_EDITORS_OBSERVER_BY_PROCESS_TYPE SYSRES_CONST_EDITORS_REFERENCE_CODE SYSRES_CONST_EDITORS_REPLACE_SPEC_CHARS_REQUISITE_CODE SYSRES_CONST_EDITORS_USE_PLUGINS_REQUISITE_CODE SYSRES_CONST_EDITORS_VIEW_DOCUMENT_OPENED_TO_EDIT_CODE SYSRES_CONST_EDOC_CARD_TYPE_REQUISITE_CODE SYSRES_CONST_EDOC_CARD_TYPES_LINK_REQUISITE_CODE SYSRES_CONST_EDOC_CERTIFICATE_AND_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_CERTIFICATE_ENCODE_CODE SYSRES_CONST_EDOC_DATE_REQUISITE_CODE SYSRES_CONST_EDOC_KIND_REFERENCE_CODE SYSRES_CONST_EDOC_KINDS_BY_TEMPLATE_ACTION_CODE SYSRES_CONST_EDOC_MANAGE_ACCESS_CODE SYSRES_CONST_EDOC_NONE_ENCODE_CODE SYSRES_CONST_EDOC_NUMBER_REQUISITE_CODE SYSRES_CONST_EDOC_PASSWORD_ENCODE_CODE SYSRES_CONST_EDOC_READONLY_ACCESS_CODE SYSRES_CONST_EDOC_SHELL_LIFE_TYPE_VIEW_VALUE SYSRES_CONST_EDOC_SIZE_RESTRICTION_PRIORITY_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_CHECK_ACCESS_RIGHTS_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_COMPUTER_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_DATABASE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_EDIT_IN_STORAGE_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_LOCAL_PATH_REQUISITE_CODE SYSRES_CONST_EDOC_STORAGE_SHARED_SOURCE_NAME_REQUISITE_CODE SYSRES_CONST_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_EDOC_TYPES_REFERENCE_CODE SYSRES_CONST_EDOC_VERSION_ACTIVE_STAGE_CODE SYSRES_CONST_EDOC_VERSION_DESIGN_STAGE_CODE SYSRES_CONST_EDOC_VERSION_OBSOLETE_STAGE_CODE SYSRES_CONST_EDOC_WRITE_ACCES_CODE SYSRES_CONST_EDOCUMENT_CARD_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_END_DATE_REQUISITE_CODE SYSRES_CONST_ENUMERATION_TYPE_REQUISITE_CODE SYSRES_CONST_EXECUTE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_EXECUTIVE_FILE_STORAGE_TYPE SYSRES_CONST_EXIST_CONST SYSRES_CONST_EXIST_VALUE SYSRES_CONST_EXPORT_LOCK_TYPE_ASK SYSRES_CONST_EXPORT_LOCK_TYPE_WITH_LOCK SYSRES_CONST_EXPORT_LOCK_TYPE_WITHOUT_LOCK SYSRES_CONST_EXPORT_VERSION_TYPE_ASK SYSRES_CONST_EXPORT_VERSION_TYPE_LAST SYSRES_CONST_EXPORT_VERSION_TYPE_LAST_ACTIVE SYSRES_CONST_EXTENSION_REQUISITE_CODE SYSRES_CONST_FILTER_NAME_REQUISITE_CODE SYSRES_CONST_FILTER_REQUISITE_CODE SYSRES_CONST_FILTER_TYPE_COMMON_CODE SYSRES_CONST_FILTER_TYPE_COMMON_NAME SYSRES_CONST_FILTER_TYPE_USER_CODE SYSRES_CONST_FILTER_TYPE_USER_NAME SYSRES_CONST_FILTER_VALUE_REQUISITE_NAME SYSRES_CONST_FLOAT_NUMBER_FORMAT_CHAR SYSRES_CONST_FLOAT_REQUISITE_TYPE SYSRES_CONST_FOLDER_AUTHOR_VALUE SYSRES_CONST_FOLDER_KIND_ANY_OBJECTS SYSRES_CONST_FOLDER_KIND_COMPONENTS SYSRES_CONST_FOLDER_KIND_EDOCS SYSRES_CONST_FOLDER_KIND_JOBS SYSRES_CONST_FOLDER_KIND_TASKS SYSRES_CONST_FOLDER_TYPE_COMMON SYSRES_CONST_FOLDER_TYPE_COMPONENT SYSRES_CONST_FOLDER_TYPE_FAVORITES SYSRES_CONST_FOLDER_TYPE_INBOX SYSRES_CONST_FOLDER_TYPE_OUTBOX SYSRES_CONST_FOLDER_TYPE_QUICK_LAUNCH SYSRES_CONST_FOLDER_TYPE_SEARCH SYSRES_CONST_FOLDER_TYPE_SHORTCUTS SYSRES_CONST_FOLDER_TYPE_USER SYSRES_CONST_FROM_DICTIONARY_ENUM_METHOD_FLAG SYSRES_CONST_FULL_SUBSTITUTE_TYPE SYSRES_CONST_FULL_SUBSTITUTE_TYPE_CODE SYSRES_CONST_FUNCTION_CANCEL_RESULT SYSRES_CONST_FUNCTION_CATEGORY_SYSTEM SYSRES_CONST_FUNCTION_CATEGORY_USER SYSRES_CONST_FUNCTION_FAILURE_RESULT SYSRES_CONST_FUNCTION_SAVE_RESULT SYSRES_CONST_GENERATED_REQUISITE SYSRES_CONST_GREEN_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_GROUP_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_CODE SYSRES_CONST_GROUP_CATEGORY_NORMAL_NAME SYSRES_CONST_GROUP_CATEGORY_SERVICE_CODE SYSRES_CONST_GROUP_CATEGORY_SERVICE_NAME SYSRES_CONST_GROUP_COMMON_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_FULL_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_CODES_REQUISITE_CODE SYSRES_CONST_GROUP_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_GROUP_SERVICE_CATEGORY_FIELD_VALUE SYSRES_CONST_GROUP_USER_REQUISITE_CODE SYSRES_CONST_GROUPS_REFERENCE_CODE SYSRES_CONST_GROUPS_REQUISITE_CODE SYSRES_CONST_HIDDEN_MODE_NAME SYSRES_CONST_HIGH_LVL_REQUISITE_CODE SYSRES_CONST_HISTORY_ACTION_CREATE_CODE SYSRES_CONST_HISTORY_ACTION_DELETE_CODE SYSRES_CONST_HISTORY_ACTION_EDIT_CODE SYSRES_CONST_HOUR_CHAR SYSRES_CONST_ID_REQUISITE_CODE SYSRES_CONST_IDSPS_REQUISITE_CODE SYSRES_CONST_IMAGE_MODE_COLOR SYSRES_CONST_IMAGE_MODE_GREYSCALE SYSRES_CONST_IMAGE_MODE_MONOCHROME SYSRES_CONST_IMPORTANCE_HIGH SYSRES_CONST_IMPORTANCE_LOW SYSRES_CONST_IMPORTANCE_NORMAL SYSRES_CONST_IN_DESIGN_VERSION_STATE_PICK_VALUE SYSRES_CONST_INCOMING_WORK_RULE_TYPE_CODE SYSRES_CONST_INT_REQUISITE SYSRES_CONST_INT_REQUISITE_TYPE SYSRES_CONST_INTEGER_NUMBER_FORMAT_CHAR SYSRES_CONST_INTEGER_TYPE_CHAR SYSRES_CONST_IS_GENERATED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_PUBLIC_ROLE_REQUISITE_CODE SYSRES_CONST_IS_REMOTE_USER_NEGATIVE_VALUE SYSRES_CONST_IS_REMOTE_USER_POSITIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_NEGATIVE_VALUE SYSRES_CONST_IS_STORED_REQUISITE_STORED_VALUE SYSRES_CONST_ITALIC_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_JOB_BLOCK_DESCRIPTION SYSRES_CONST_JOB_KIND_CONTROL_JOB SYSRES_CONST_JOB_KIND_JOB SYSRES_CONST_JOB_KIND_NOTICE SYSRES_CONST_JOB_STATE_ABORTED SYSRES_CONST_JOB_STATE_COMPLETE SYSRES_CONST_JOB_STATE_WORKING SYSRES_CONST_KIND_REQUISITE_CODE SYSRES_CONST_KIND_REQUISITE_NAME SYSRES_CONST_KINDS_CREATE_SHADOW_COPIES_REQUISITE_CODE SYSRES_CONST_KINDS_DEFAULT_EDOC_LIFE_STAGE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALL_TEPLATES_ALLOWED_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_LIFE_CYCLE_STAGE_CHANGING_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_ALLOW_MULTIPLE_ACTIVE_VERSIONS_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_SHARE_ACCES_RIGHTS_BY_DEFAULT_CODE SYSRES_CONST_KINDS_EDOC_TEMPLATE_REQUISITE_CODE SYSRES_CONST_KINDS_EDOC_TYPE_REQUISITE_CODE SYSRES_CONST_KINDS_SIGNERS_REQUISITES_CODE SYSRES_CONST_KOD_INPUT_TYPE SYSRES_CONST_LAST_UPDATE_DATE_REQUISITE_CODE SYSRES_CONST_LIFE_CYCLE_START_STAGE_REQUISITE_CODE SYSRES_CONST_LILAC_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_LINK_OBJECT_KIND_COMPONENT SYSRES_CONST_LINK_OBJECT_KIND_DOCUMENT SYSRES_CONST_LINK_OBJECT_KIND_EDOC SYSRES_CONST_LINK_OBJECT_KIND_FOLDER SYSRES_CONST_LINK_OBJECT_KIND_JOB SYSRES_CONST_LINK_OBJECT_KIND_REFERENCE SYSRES_CONST_LINK_OBJECT_KIND_TASK SYSRES_CONST_LINK_REF_TYPE_REQUISITE_CODE SYSRES_CONST_LIST_REFERENCE_MODE_NAME SYSRES_CONST_LOCALIZATION_DICTIONARY_MAIN_VIEW_CODE SYSRES_CONST_MAIN_VIEW_CODE SYSRES_CONST_MANUAL_ENUM_METHOD_FLAG SYSRES_CONST_MASTER_COMP_TYPE_REQUISITE_CODE SYSRES_CONST_MASTER_TABLE_REC_ID_REQUISITE_CODE SYSRES_CONST_MAXIMIZED_MODE_NAME SYSRES_CONST_ME_VALUE SYSRES_CONST_MESSAGE_ATTENTION_CAPTION SYSRES_CONST_MESSAGE_CONFIRMATION_CAPTION SYSRES_CONST_MESSAGE_ERROR_CAPTION SYSRES_CONST_MESSAGE_INFORMATION_CAPTION SYSRES_CONST_MINIMIZED_MODE_NAME SYSRES_CONST_MINUTE_CHAR SYSRES_CONST_MODULE_REQUISITE_CODE SYSRES_CONST_MONITORING_BLOCK_DESCRIPTION SYSRES_CONST_MONTH_FORMAT_VALUE SYSRES_CONST_NAME_LOCALIZE_ID_REQUISITE_CODE SYSRES_CONST_NAME_REQUISITE_CODE SYSRES_CONST_NAME_SINGULAR_REQUISITE_CODE SYSRES_CONST_NAMEAN_INPUT_TYPE SYSRES_CONST_NEGATIVE_PICK_VALUE SYSRES_CONST_NEGATIVE_VALUE SYSRES_CONST_NO SYSRES_CONST_NO_PICK_VALUE SYSRES_CONST_NO_SIGNATURE_REQUISITE_CODE SYSRES_CONST_NO_VALUE SYSRES_CONST_NONE_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_NORMAL_ACCESS_RIGHTS_TYPE_CODE SYSRES_CONST_NORMAL_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_NORMAL_MODE_NAME SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_CODE SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_NAME SYSRES_CONST_NOTE_REQUISITE_CODE SYSRES_CONST_NOTICE_BLOCK_DESCRIPTION SYSRES_CONST_NUM_REQUISITE SYSRES_CONST_NUM_STR_REQUISITE_CODE SYSRES_CONST_NUMERATION_AUTO_NOT_STRONG SYSRES_CONST_NUMERATION_AUTO_STRONG SYSRES_CONST_NUMERATION_FROM_DICTONARY SYSRES_CONST_NUMERATION_MANUAL SYSRES_CONST_NUMERIC_TYPE_CHAR SYSRES_CONST_NUMREQ_REQUISITE_CODE SYSRES_CONST_OBSOLETE_VERSION_STATE_PICK_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_FEMININE SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_MASCULINE SYSRES_CONST_OPTIONAL_FORM_COMP_REQCODE_PREFIX SYSRES_CONST_ORANGE_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_ORIGINALREF_REQUISITE_CODE SYSRES_CONST_OURFIRM_REF_CODE SYSRES_CONST_OURFIRM_REQUISITE_CODE SYSRES_CONST_OURFIRM_VAR SYSRES_CONST_OUTGOING_WORK_RULE_TYPE_CODE SYSRES_CONST_PICK_NEGATIVE_RESULT SYSRES_CONST_PICK_POSITIVE_RESULT SYSRES_CONST_PICK_REQUISITE SYSRES_CONST_PICK_REQUISITE_TYPE SYSRES_CONST_PICK_TYPE_CHAR SYSRES_CONST_PLAN_STATUS_REQUISITE_CODE SYSRES_CONST_PLATFORM_VERSION_COMMENT SYSRES_CONST_PLUGINS_SETTINGS_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_POSITIVE_PICK_VALUE SYSRES_CONST_POWER_TO_CREATE_ACTION_CODE SYSRES_CONST_POWER_TO_SIGN_ACTION_CODE SYSRES_CONST_PRIORITY_REQUISITE_CODE SYSRES_CONST_QUALIFIED_TASK_TYPE SYSRES_CONST_QUALIFIED_TASK_TYPE_CODE SYSRES_CONST_RECSTAT_REQUISITE_CODE SYSRES_CONST_RED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_REF_ID_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_REF_REQUISITE SYSRES_CONST_REF_REQUISITE_TYPE SYSRES_CONST_REF_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE SYSRES_CONST_REFERENCE_RECORD_HISTORY_CREATE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_DELETE_ACTION_CODE SYSRES_CONST_REFERENCE_RECORD_HISTORY_MODIFY_ACTION_CODE SYSRES_CONST_REFERENCE_TYPE_CHAR SYSRES_CONST_REFERENCE_TYPE_REQUISITE_NAME SYSRES_CONST_REFERENCES_ADD_PARAMS_REQUISITE_CODE SYSRES_CONST_REFERENCES_DISPLAY_REQUISITE_REQUISITE_CODE SYSRES_CONST_REMOTE_SERVER_STATUS_WORKING SYSRES_CONST_REMOTE_SERVER_TYPE_MAIN SYSRES_CONST_REMOTE_SERVER_TYPE_SECONDARY SYSRES_CONST_REMOTE_USER_FLAG_VALUE_CODE SYSRES_CONST_REPORT_APP_EDITOR_INTERNAL SYSRES_CONST_REPORT_BASE_REPORT_ID_REQUISITE_CODE SYSRES_CONST_REPORT_BASE_REPORT_REQUISITE_CODE SYSRES_CONST_REPORT_SCRIPT_REQUISITE_CODE SYSRES_CONST_REPORT_TEMPLATE_REQUISITE_CODE SYSRES_CONST_REPORT_VIEWER_CODE_REQUISITE_CODE SYSRES_CONST_REQ_ALLOW_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_RECORD_DEFAULT_VALUE SYSRES_CONST_REQ_ALLOW_SERVER_COMPONENT_DEFAULT_VALUE SYSRES_CONST_REQ_MODE_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_EDIT_CODE SYSRES_CONST_REQ_MODE_HIDDEN_CODE SYSRES_CONST_REQ_MODE_NOT_AVAILABLE_CODE SYSRES_CONST_REQ_MODE_VIEW_CODE SYSRES_CONST_REQ_NUMBER_REQUISITE_CODE SYSRES_CONST_REQ_SECTION_VALUE SYSRES_CONST_REQ_TYPE_VALUE SYSRES_CONST_REQUISITE_FORMAT_BY_UNIT SYSRES_CONST_REQUISITE_FORMAT_DATE_FULL SYSRES_CONST_REQUISITE_FORMAT_DATE_TIME SYSRES_CONST_REQUISITE_FORMAT_LEFT SYSRES_CONST_REQUISITE_FORMAT_RIGHT SYSRES_CONST_REQUISITE_FORMAT_WITHOUT_UNIT SYSRES_CONST_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_REQUISITE_SECTION_ACTIONS SYSRES_CONST_REQUISITE_SECTION_BUTTON SYSRES_CONST_REQUISITE_SECTION_BUTTONS SYSRES_CONST_REQUISITE_SECTION_CARD SYSRES_CONST_REQUISITE_SECTION_TABLE SYSRES_CONST_REQUISITE_SECTION_TABLE10 SYSRES_CONST_REQUISITE_SECTION_TABLE11 SYSRES_CONST_REQUISITE_SECTION_TABLE12 SYSRES_CONST_REQUISITE_SECTION_TABLE13 SYSRES_CONST_REQUISITE_SECTION_TABLE14 SYSRES_CONST_REQUISITE_SECTION_TABLE15 SYSRES_CONST_REQUISITE_SECTION_TABLE16 SYSRES_CONST_REQUISITE_SECTION_TABLE17 SYSRES_CONST_REQUISITE_SECTION_TABLE18 SYSRES_CONST_REQUISITE_SECTION_TABLE19 SYSRES_CONST_REQUISITE_SECTION_TABLE2 SYSRES_CONST_REQUISITE_SECTION_TABLE20 SYSRES_CONST_REQUISITE_SECTION_TABLE21 SYSRES_CONST_REQUISITE_SECTION_TABLE22 SYSRES_CONST_REQUISITE_SECTION_TABLE23 SYSRES_CONST_REQUISITE_SECTION_TABLE24 SYSRES_CONST_REQUISITE_SECTION_TABLE3 SYSRES_CONST_REQUISITE_SECTION_TABLE4 SYSRES_CONST_REQUISITE_SECTION_TABLE5 SYSRES_CONST_REQUISITE_SECTION_TABLE6 SYSRES_CONST_REQUISITE_SECTION_TABLE7 SYSRES_CONST_REQUISITE_SECTION_TABLE8 SYSRES_CONST_REQUISITE_SECTION_TABLE9 SYSRES_CONST_REQUISITES_PSEUDOREFERENCE_REQUISITE_NUMBER_REQUISITE_CODE SYSRES_CONST_RIGHT_ALIGNMENT_CODE SYSRES_CONST_ROLES_REFERENCE_CODE SYSRES_CONST_ROUTE_STEP_AFTER_RUS SYSRES_CONST_ROUTE_STEP_AND_CONDITION_RUS SYSRES_CONST_ROUTE_STEP_OR_CONDITION_RUS SYSRES_CONST_ROUTE_TYPE_COMPLEX SYSRES_CONST_ROUTE_TYPE_PARALLEL SYSRES_CONST_ROUTE_TYPE_SERIAL SYSRES_CONST_SBDATASETDESC_NEGATIVE_VALUE SYSRES_CONST_SBDATASETDESC_POSITIVE_VALUE SYSRES_CONST_SBVIEWSDESC_POSITIVE_VALUE SYSRES_CONST_SCRIPT_BLOCK_DESCRIPTION SYSRES_CONST_SEARCH_BY_TEXT_REQUISITE_CODE SYSRES_CONST_SEARCHES_COMPONENT_CONTENT SYSRES_CONST_SEARCHES_CRITERIA_ACTION_NAME SYSRES_CONST_SEARCHES_EDOC_CONTENT SYSRES_CONST_SEARCHES_FOLDER_CONTENT SYSRES_CONST_SEARCHES_JOB_CONTENT SYSRES_CONST_SEARCHES_REFERENCE_CODE SYSRES_CONST_SEARCHES_TASK_CONTENT SYSRES_CONST_SECOND_CHAR SYSRES_CONST_SECTION_REQUISITE_ACTIONS_VALUE SYSRES_CONST_SECTION_REQUISITE_CARD_VALUE SYSRES_CONST_SECTION_REQUISITE_CODE SYSRES_CONST_SECTION_REQUISITE_DETAIL_1_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_2_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_3_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_4_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_5_VALUE SYSRES_CONST_SECTION_REQUISITE_DETAIL_6_VALUE SYSRES_CONST_SELECT_REFERENCE_MODE_NAME SYSRES_CONST_SELECT_TYPE_SELECTABLE SYSRES_CONST_SELECT_TYPE_SELECTABLE_ONLY_CHILD SYSRES_CONST_SELECT_TYPE_SELECTABLE_WITH_CHILD SYSRES_CONST_SELECT_TYPE_UNSLECTABLE SYSRES_CONST_SERVER_TYPE_MAIN SYSRES_CONST_SERVICE_USER_CATEGORY_FIELD_VALUE SYSRES_CONST_SETTINGS_USER_REQUISITE_CODE SYSRES_CONST_SIGNATURE_AND_ENCODE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SIGNATURE_CERTIFICATE_TYPE_CODE SYSRES_CONST_SINGULAR_TITLE_REQUISITE_CODE SYSRES_CONST_SQL_SERVER_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_SQL_SERVER_ENCODE_AUTHENTIFICATION_FLAG_VALUE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_CODE SYSRES_CONST_STANDART_ROUTE_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_STANDART_ROUTES_GROUPS_REFERENCE_CODE SYSRES_CONST_STATE_REQ_NAME SYSRES_CONST_STATE_REQUISITE_ACTIVE_VALUE SYSRES_CONST_STATE_REQUISITE_CLOSED_VALUE SYSRES_CONST_STATE_REQUISITE_CODE SYSRES_CONST_STATIC_ROLE_TYPE_CODE SYSRES_CONST_STATUS_PLAN_DEFAULT_VALUE SYSRES_CONST_STATUS_VALUE_AUTOCLEANING SYSRES_CONST_STATUS_VALUE_BLUE_SQUARE SYSRES_CONST_STATUS_VALUE_COMPLETE SYSRES_CONST_STATUS_VALUE_GREEN_SQUARE SYSRES_CONST_STATUS_VALUE_ORANGE_SQUARE SYSRES_CONST_STATUS_VALUE_PURPLE_SQUARE SYSRES_CONST_STATUS_VALUE_RED_SQUARE SYSRES_CONST_STATUS_VALUE_SUSPEND SYSRES_CONST_STATUS_VALUE_YELLOW_SQUARE SYSRES_CONST_STDROUTE_SHOW_TO_USERS_REQUISITE_CODE SYSRES_CONST_STORAGE_TYPE_FILE SYSRES_CONST_STORAGE_TYPE_SQL_SERVER SYSRES_CONST_STR_REQUISITE SYSRES_CONST_STRIKEOUT_LIFE_CYCLE_STAGE_DRAW_STYLE SYSRES_CONST_STRING_FORMAT_LEFT_ALIGN_CHAR SYSRES_CONST_STRING_FORMAT_RIGHT_ALIGN_CHAR SYSRES_CONST_STRING_REQUISITE_CODE SYSRES_CONST_STRING_REQUISITE_TYPE SYSRES_CONST_STRING_TYPE_CHAR SYSRES_CONST_SUBSTITUTES_PSEUDOREFERENCE_CODE SYSRES_CONST_SUBTASK_BLOCK_DESCRIPTION SYSRES_CONST_SYSTEM_SETTING_CURRENT_USER_PARAM_VALUE SYSRES_CONST_SYSTEM_SETTING_EMPTY_VALUE_PARAM_VALUE SYSRES_CONST_SYSTEM_VERSION_COMMENT SYSRES_CONST_TASK_ACCESS_TYPE_ALL SYSRES_CONST_TASK_ACCESS_TYPE_ALL_MEMBERS SYSRES_CONST_TASK_ACCESS_TYPE_MANUAL SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION_AND_PASSWORD SYSRES_CONST_TASK_ENCODE_TYPE_NONE SYSRES_CONST_TASK_ENCODE_TYPE_PASSWORD SYSRES_CONST_TASK_ROUTE_ALL_CONDITION SYSRES_CONST_TASK_ROUTE_AND_CONDITION SYSRES_CONST_TASK_ROUTE_OR_CONDITION SYSRES_CONST_TASK_STATE_ABORTED SYSRES_CONST_TASK_STATE_COMPLETE SYSRES_CONST_TASK_STATE_CONTINUED SYSRES_CONST_TASK_STATE_CONTROL SYSRES_CONST_TASK_STATE_INIT SYSRES_CONST_TASK_STATE_WORKING SYSRES_CONST_TASK_TITLE SYSRES_CONST_TASK_TYPES_GROUPS_REFERENCE_CODE SYSRES_CONST_TASK_TYPES_REFERENCE_CODE SYSRES_CONST_TEMPLATES_REFERENCE_CODE SYSRES_CONST_TEST_DATE_REQUISITE_NAME SYSRES_CONST_TEST_DEV_DATABASE_NAME SYSRES_CONST_TEST_DEV_SYSTEM_CODE SYSRES_CONST_TEST_EDMS_DATABASE_NAME SYSRES_CONST_TEST_EDMS_MAIN_CODE SYSRES_CONST_TEST_EDMS_MAIN_DB_NAME SYSRES_CONST_TEST_EDMS_SECOND_CODE SYSRES_CONST_TEST_EDMS_SECOND_DB_NAME SYSRES_CONST_TEST_EDMS_SYSTEM_CODE SYSRES_CONST_TEST_NUMERIC_REQUISITE_NAME SYSRES_CONST_TEXT_REQUISITE SYSRES_CONST_TEXT_REQUISITE_CODE SYSRES_CONST_TEXT_REQUISITE_TYPE SYSRES_CONST_TEXT_TYPE_CHAR SYSRES_CONST_TYPE_CODE_REQUISITE_CODE SYSRES_CONST_TYPE_REQUISITE_CODE SYSRES_CONST_UNDEFINED_LIFE_CYCLE_STAGE_FONT_COLOR SYSRES_CONST_UNITS_SECTION_ID_REQUISITE_CODE SYSRES_CONST_UNITS_SECTION_REQUISITE_CODE SYSRES_CONST_UNOPERATING_RECORD_FLAG_VALUE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_CODE SYSRES_CONST_UNSTORED_DATA_REQUISITE_NAME SYSRES_CONST_USE_ACCESS_TYPE_CODE SYSRES_CONST_USE_ACCESS_TYPE_NAME SYSRES_CONST_USER_ACCOUNT_TYPE_VALUE_CODE SYSRES_CONST_USER_ADDITIONAL_INFORMATION_REQUISITE_CODE SYSRES_CONST_USER_AND_GROUP_ID_FROM_PSEUDOREFERENCE_REQUISITE_CODE SYSRES_CONST_USER_CATEGORY_NORMAL SYSRES_CONST_USER_CERTIFICATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_STATE_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_SUBJECT_NAME_REQUISITE_CODE SYSRES_CONST_USER_CERTIFICATE_THUMBPRINT_REQUISITE_CODE SYSRES_CONST_USER_COMMON_CATEGORY SYSRES_CONST_USER_COMMON_CATEGORY_CODE SYSRES_CONST_USER_FULL_NAME_REQUISITE_CODE SYSRES_CONST_USER_GROUP_TYPE_REQUISITE_CODE SYSRES_CONST_USER_LOGIN_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USER_REMOTE_SYSTEM_REQUISITE_CODE SYSRES_CONST_USER_RIGHTS_T_REQUISITE_CODE SYSRES_CONST_USER_SERVER_NAME_REQUISITE_CODE SYSRES_CONST_USER_SERVICE_CATEGORY SYSRES_CONST_USER_SERVICE_CATEGORY_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_CODE SYSRES_CONST_USER_STATUS_ADMINISTRATOR_NAME SYSRES_CONST_USER_STATUS_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_DEVELOPER_NAME SYSRES_CONST_USER_STATUS_DISABLED_CODE SYSRES_CONST_USER_STATUS_DISABLED_NAME SYSRES_CONST_USER_STATUS_SYSTEM_DEVELOPER_CODE SYSRES_CONST_USER_STATUS_USER_CODE SYSRES_CONST_USER_STATUS_USER_NAME SYSRES_CONST_USER_STATUS_USER_NAME_DEPRECATED SYSRES_CONST_USER_TYPE_FIELD_VALUE_USER SYSRES_CONST_USER_TYPE_REQUISITE_CODE SYSRES_CONST_USERS_CONTROLLER_REQUISITE_CODE SYSRES_CONST_USERS_IS_MAIN_SERVER_REQUISITE_CODE SYSRES_CONST_USERS_REFERENCE_CODE SYSRES_CONST_USERS_REGISTRATION_CERTIFICATES_ACTION_NAME SYSRES_CONST_USERS_REQUISITE_CODE SYSRES_CONST_USERS_SYSTEM_REQUISITE_CODE SYSRES_CONST_USERS_USER_ACCESS_RIGHTS_TYPR_REQUISITE_CODE SYSRES_CONST_USERS_USER_AUTHENTICATION_REQUISITE_CODE SYSRES_CONST_USERS_USER_COMPONENT_REQUISITE_CODE SYSRES_CONST_USERS_USER_GROUP_REQUISITE_CODE SYSRES_CONST_USERS_VIEW_CERTIFICATES_ACTION_NAME SYSRES_CONST_VIEW_DEFAULT_CODE SYSRES_CONST_VIEW_DEFAULT_NAME SYSRES_CONST_VIEWER_REQUISITE_CODE SYSRES_CONST_WAITING_BLOCK_DESCRIPTION SYSRES_CONST_WIZARD_FORM_LABEL_TEST_STRING  SYSRES_CONST_WIZARD_QUERY_PARAM_HEIGHT_ETALON_STRING SYSRES_CONST_WIZARD_REFERENCE_COMMENT_REQUISITE_CODE SYSRES_CONST_WORK_RULES_DESCRIPTION_REQUISITE_CODE SYSRES_CONST_WORK_TIME_CALENDAR_REFERENCE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORK_WORKFLOW_SOFT_ROUTE_TYPE_VALUE_CODE_RUS SYSRES_CONST_WORKFLOW_ROUTE_TYPR_HARD SYSRES_CONST_WORKFLOW_ROUTE_TYPR_SOFT SYSRES_CONST_XML_ENCODING SYSRES_CONST_XREC_STAT_REQUISITE_CODE SYSRES_CONST_XRECID_FIELD_NAME SYSRES_CONST_YES SYSRES_CONST_YES_NO_2_REQUISITE_CODE SYSRES_CONST_YES_NO_REQUISITE_CODE SYSRES_CONST_YES_NO_T_REF_TYPE_REQUISITE_CODE SYSRES_CONST_YES_PICK_VALUE SYSRES_CONST_YES_VALUE CR FALSE nil NO_VALUE NULL TAB TRUE YES_VALUE ADMINISTRATORS_GROUP_NAME CUSTOMIZERS_GROUP_NAME DEVELOPERS_GROUP_NAME SERVICE_USERS_GROUP_NAME DECISION_BLOCK_FIRST_OPERAND_PROPERTY DECISION_BLOCK_NAME_PROPERTY DECISION_BLOCK_OPERATION_PROPERTY DECISION_BLOCK_RESULT_TYPE_PROPERTY DECISION_BLOCK_SECOND_OPERAND_PROPERTY ANY_FILE_EXTENTION COMPRESSED_DOCUMENT_EXTENSION EXTENDED_DOCUMENT_EXTENSION SHORT_COMPRESSED_DOCUMENT_EXTENSION SHORT_EXTENDED_DOCUMENT_EXTENSION JOB_BLOCK_ABORT_DEADLINE_PROPERTY JOB_BLOCK_AFTER_FINISH_EVENT JOB_BLOCK_AFTER_QUERY_PARAMETERS_EVENT JOB_BLOCK_ATTACHMENT_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY JOB_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY JOB_BLOCK_BEFORE_QUERY_PARAMETERS_EVENT JOB_BLOCK_BEFORE_START_EVENT JOB_BLOCK_CREATED_JOBS_PROPERTY JOB_BLOCK_DEADLINE_PROPERTY JOB_BLOCK_EXECUTION_RESULTS_PROPERTY JOB_BLOCK_IS_PARALLEL_PROPERTY JOB_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY JOB_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY JOB_BLOCK_JOB_TEXT_PROPERTY JOB_BLOCK_NAME_PROPERTY JOB_BLOCK_NEED_SIGN_ON_PERFORM_PROPERTY JOB_BLOCK_PERFORMER_PROPERTY JOB_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY JOB_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY JOB_BLOCK_SUBJECT_PROPERTY ENGLISH_LANGUAGE_CODE RUSSIAN_LANGUAGE_CODE smHidden smMaximized smMinimized smNormal wmNo wmYes COMPONENT_TOKEN_LINK_KIND DOCUMENT_LINK_KIND EDOCUMENT_LINK_KIND FOLDER_LINK_KIND JOB_LINK_KIND REFERENCE_LINK_KIND TASK_LINK_KIND COMPONENT_TOKEN_LOCK_TYPE EDOCUMENT_VERSION_LOCK_TYPE MONITOR_BLOCK_AFTER_FINISH_EVENT MONITOR_BLOCK_BEFORE_START_EVENT MONITOR_BLOCK_DEADLINE_PROPERTY MONITOR_BLOCK_INTERVAL_PROPERTY MONITOR_BLOCK_INTERVAL_TYPE_PROPERTY MONITOR_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY MONITOR_BLOCK_NAME_PROPERTY MONITOR_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY MONITOR_BLOCK_SEARCH_SCRIPT_PROPERTY NOTICE_BLOCK_AFTER_FINISH_EVENT NOTICE_BLOCK_ATTACHMENT_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY NOTICE_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY NOTICE_BLOCK_BEFORE_START_EVENT NOTICE_BLOCK_CREATED_NOTICES_PROPERTY NOTICE_BLOCK_DEADLINE_PROPERTY NOTICE_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY NOTICE_BLOCK_NAME_PROPERTY NOTICE_BLOCK_NOTICE_TEXT_PROPERTY NOTICE_BLOCK_PERFORMER_PROPERTY NOTICE_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY NOTICE_BLOCK_SUBJECT_PROPERTY dseAfterCancel dseAfterClose dseAfterDelete dseAfterDeleteOutOfTransaction dseAfterInsert dseAfterOpen dseAfterScroll dseAfterUpdate dseAfterUpdateOutOfTransaction dseBeforeCancel dseBeforeClose dseBeforeDelete dseBeforeDetailUpdate dseBeforeInsert dseBeforeOpen dseBeforeUpdate dseOnAnyRequisiteChange dseOnCloseRecord dseOnDeleteError dseOnOpenRecord dseOnPrepareUpdate dseOnUpdateError dseOnUpdateRatifiedRecord dseOnValidDelete dseOnValidUpdate reOnChange reOnChangeValues SELECTION_BEGIN_ROUTE_EVENT SELECTION_END_ROUTE_EVENT CURRENT_PERIOD_IS_REQUIRED PREVIOUS_CARD_TYPE_NAME SHOW_RECORD_PROPERTIES_FORM ACCESS_RIGHTS_SETTING_DIALOG_CODE ADMINISTRATOR_USER_CODE ANALYTIC_REPORT_TYPE asrtHideLocal asrtHideRemote CALCULATED_ROLE_TYPE_CODE COMPONENTS_REFERENCE_DEVELOPER_VIEW_CODE DCTS_TEST_PROTOCOLS_FOLDER_PATH E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED_BY_USER E_EDOC_VERSION_ALREDY_SIGNED E_EDOC_VERSION_ALREDY_SIGNED_BY_USER EDOC_TYPES_CODE_REQUISITE_FIELD_NAME EDOCUMENTS_ALIAS_NAME FILES_FOLDER_PATH FILTER_OPERANDS_DELIMITER FILTER_OPERATIONS_DELIMITER FORMCARD_NAME FORMLIST_NAME GET_EXTENDED_DOCUMENT_EXTENSION_CREATION_MODE GET_EXTENDED_DOCUMENT_EXTENSION_IMPORT_MODE INTEGRATED_REPORT_TYPE IS_BUILDER_APPLICATION_ROLE IS_BUILDER_APPLICATION_ROLE2 IS_BUILDER_USERS ISBSYSDEV LOG_FOLDER_PATH mbCancel mbNo mbNoToAll mbOK mbYes mbYesToAll MEMORY_DATASET_DESRIPTIONS_FILENAME mrNo mrNoToAll mrYes mrYesToAll MULTIPLE_SELECT_DIALOG_CODE NONOPERATING_RECORD_FLAG_FEMININE NONOPERATING_RECORD_FLAG_MASCULINE OPERATING_RECORD_FLAG_FEMININE OPERATING_RECORD_FLAG_MASCULINE PROFILING_SETTINGS_COMMON_SETTINGS_CODE_VALUE PROGRAM_INITIATED_LOOKUP_ACTION ratDelete ratEdit ratInsert REPORT_TYPE REQUIRED_PICK_VALUES_VARIABLE rmCard rmList SBRTE_PROGID_DEV SBRTE_PROGID_RELEASE STATIC_ROLE_TYPE_CODE SUPPRESS_EMPTY_TEMPLATE_CREATION SYSTEM_USER_CODE UPDATE_DIALOG_DATASET USED_IN_OBJECT_HINT_PARAM USER_INITIATED_LOOKUP_ACTION USER_NAME_FORMAT USER_SELECTION_RESTRICTIONS WORKFLOW_TEST_PROTOCOLS_FOLDER_PATH ELS_SUBTYPE_CONTROL_NAME ELS_FOLDER_KIND_CONTROL_NAME REPEAT_PROCESS_CURRENT_OBJECT_EXCEPTION_NAME PRIVILEGE_COMPONENT_FULL_ACCESS PRIVILEGE_DEVELOPMENT_EXPORT PRIVILEGE_DEVELOPMENT_IMPORT PRIVILEGE_DOCUMENT_DELETE PRIVILEGE_ESD PRIVILEGE_FOLDER_DELETE PRIVILEGE_MANAGE_ACCESS_RIGHTS PRIVILEGE_MANAGE_REPLICATION PRIVILEGE_MANAGE_SESSION_SERVER PRIVILEGE_OBJECT_FULL_ACCESS PRIVILEGE_OBJECT_VIEW PRIVILEGE_RESERVE_LICENSE PRIVILEGE_SYSTEM_CUSTOMIZE PRIVILEGE_SYSTEM_DEVELOP PRIVILEGE_SYSTEM_INSTALL PRIVILEGE_TASK_DELETE PRIVILEGE_USER_PLUGIN_SETTINGS_CUSTOMIZE PRIVILEGES_PSEUDOREFERENCE_CODE ACCESS_TYPES_PSEUDOREFERENCE_CODE ALL_AVAILABLE_COMPONENTS_PSEUDOREFERENCE_CODE ALL_AVAILABLE_PRIVILEGES_PSEUDOREFERENCE_CODE ALL_REPLICATE_COMPONENTS_PSEUDOREFERENCE_CODE AVAILABLE_DEVELOPERS_COMPONENTS_PSEUDOREFERENCE_CODE COMPONENTS_PSEUDOREFERENCE_CODE FILTRATER_SETTINGS_CONFLICTS_PSEUDOREFERENCE_CODE GROUPS_PSEUDOREFERENCE_CODE RECEIVE_PROTOCOL_PSEUDOREFERENCE_CODE REFERENCE_REQUISITE_PSEUDOREFERENCE_CODE REFERENCE_REQUISITES_PSEUDOREFERENCE_CODE REFTYPES_PSEUDOREFERENCE_CODE REPLICATION_SEANCES_DIARY_PSEUDOREFERENCE_CODE SEND_PROTOCOL_PSEUDOREFERENCE_CODE SUBSTITUTES_PSEUDOREFERENCE_CODE SYSTEM_SETTINGS_PSEUDOREFERENCE_CODE UNITS_PSEUDOREFERENCE_CODE USERS_PSEUDOREFERENCE_CODE VIEWERS_PSEUDOREFERENCE_CODE CERTIFICATE_TYPE_ENCRYPT CERTIFICATE_TYPE_SIGN CERTIFICATE_TYPE_SIGN_AND_ENCRYPT STORAGE_TYPE_FILE STORAGE_TYPE_NAS_CIFS STORAGE_TYPE_SAPERION STORAGE_TYPE_SQL_SERVER COMPTYPE2_REQUISITE_DOCUMENTS_VALUE COMPTYPE2_REQUISITE_TASKS_VALUE COMPTYPE2_REQUISITE_FOLDERS_VALUE COMPTYPE2_REQUISITE_REFERENCES_VALUE SYSREQ_CODE SYSREQ_COMPTYPE2 SYSREQ_CONST_AVAILABLE_FOR_WEB SYSREQ_CONST_COMMON_CODE SYSREQ_CONST_COMMON_VALUE SYSREQ_CONST_FIRM_CODE SYSREQ_CONST_FIRM_STATUS SYSREQ_CONST_FIRM_VALUE SYSREQ_CONST_SERVER_STATUS SYSREQ_CONTENTS SYSREQ_DATE_OPEN SYSREQ_DATE_CLOSE SYSREQ_DESCRIPTION SYSREQ_DESCRIPTION_LOCALIZE_ID SYSREQ_DOUBLE SYSREQ_EDOC_ACCESS_TYPE SYSREQ_EDOC_AUTHOR SYSREQ_EDOC_CREATED SYSREQ_EDOC_DELEGATE_RIGHTS_REQUISITE_CODE SYSREQ_EDOC_EDITOR SYSREQ_EDOC_ENCODE_TYPE SYSREQ_EDOC_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_EXPORT_DATE SYSREQ_EDOC_EXPORTER SYSREQ_EDOC_KIND SYSREQ_EDOC_LIFE_STAGE_NAME SYSREQ_EDOC_LOCKED_FOR_SERVER_CODE SYSREQ_EDOC_MODIFIED SYSREQ_EDOC_NAME SYSREQ_EDOC_NOTE SYSREQ_EDOC_QUALIFIED_ID SYSREQ_EDOC_SESSION_KEY SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_NAME SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_VERSION SYSREQ_EDOC_SIGNATURE_TYPE SYSREQ_EDOC_SIGNED SYSREQ_EDOC_STORAGE SYSREQ_EDOC_STORAGES_ARCHIVE_STORAGE SYSREQ_EDOC_STORAGES_CHECK_RIGHTS SYSREQ_EDOC_STORAGES_COMPUTER_NAME SYSREQ_EDOC_STORAGES_EDIT_IN_STORAGE SYSREQ_EDOC_STORAGES_EXECUTIVE_STORAGE SYSREQ_EDOC_STORAGES_FUNCTION SYSREQ_EDOC_STORAGES_INITIALIZED SYSREQ_EDOC_STORAGES_LOCAL_PATH SYSREQ_EDOC_STORAGES_SAPERION_DATABASE_NAME SYSREQ_EDOC_STORAGES_SEARCH_BY_TEXT SYSREQ_EDOC_STORAGES_SERVER_NAME SYSREQ_EDOC_STORAGES_SHARED_SOURCE_NAME SYSREQ_EDOC_STORAGES_TYPE SYSREQ_EDOC_TEXT_MODIFIED SYSREQ_EDOC_TYPE_ACT_CODE SYSREQ_EDOC_TYPE_ACT_DESCRIPTION SYSREQ_EDOC_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_EDOC_TYPE_ACT_SECTION SYSREQ_EDOC_TYPE_ADD_PARAMS SYSREQ_EDOC_TYPE_COMMENT SYSREQ_EDOC_TYPE_EVENT_TEXT SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_EDOC_TYPE_NAME_LOCALIZE_ID SYSREQ_EDOC_TYPE_NUMERATION_METHOD SYSREQ_EDOC_TYPE_PSEUDO_REQUISITE_CODE SYSREQ_EDOC_TYPE_REQ_CODE SYSREQ_EDOC_TYPE_REQ_DESCRIPTION SYSREQ_EDOC_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_EDOC_TYPE_REQ_IS_LEADING SYSREQ_EDOC_TYPE_REQ_IS_REQUIRED SYSREQ_EDOC_TYPE_REQ_NUMBER SYSREQ_EDOC_TYPE_REQ_ON_CHANGE SYSREQ_EDOC_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_EDOC_TYPE_REQ_ON_SELECT SYSREQ_EDOC_TYPE_REQ_ON_SELECT_KIND SYSREQ_EDOC_TYPE_REQ_SECTION SYSREQ_EDOC_TYPE_VIEW_CARD SYSREQ_EDOC_TYPE_VIEW_CODE SYSREQ_EDOC_TYPE_VIEW_COMMENT SYSREQ_EDOC_TYPE_VIEW_IS_MAIN SYSREQ_EDOC_TYPE_VIEW_NAME SYSREQ_EDOC_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_EDOC_VERSION_AUTHOR SYSREQ_EDOC_VERSION_CRC SYSREQ_EDOC_VERSION_DATA SYSREQ_EDOC_VERSION_EDITOR SYSREQ_EDOC_VERSION_EXPORT_DATE SYSREQ_EDOC_VERSION_EXPORTER SYSREQ_EDOC_VERSION_HIDDEN SYSREQ_EDOC_VERSION_LIFE_STAGE SYSREQ_EDOC_VERSION_MODIFIED SYSREQ_EDOC_VERSION_NOTE SYSREQ_EDOC_VERSION_SIGNATURE_TYPE SYSREQ_EDOC_VERSION_SIGNED SYSREQ_EDOC_VERSION_SIZE SYSREQ_EDOC_VERSION_SOURCE SYSREQ_EDOC_VERSION_TEXT_MODIFIED SYSREQ_EDOCKIND_DEFAULT_VERSION_STATE_CODE SYSREQ_FOLDER_KIND SYSREQ_FUNC_CATEGORY SYSREQ_FUNC_COMMENT SYSREQ_FUNC_GROUP SYSREQ_FUNC_GROUP_COMMENT SYSREQ_FUNC_GROUP_NUMBER SYSREQ_FUNC_HELP SYSREQ_FUNC_PARAM_DEF_VALUE SYSREQ_FUNC_PARAM_IDENT SYSREQ_FUNC_PARAM_NUMBER SYSREQ_FUNC_PARAM_TYPE SYSREQ_FUNC_TEXT SYSREQ_GROUP_CATEGORY SYSREQ_ID SYSREQ_LAST_UPDATE SYSREQ_LEADER_REFERENCE SYSREQ_LINE_NUMBER SYSREQ_MAIN_RECORD_ID SYSREQ_NAME SYSREQ_NAME_LOCALIZE_ID SYSREQ_NOTE SYSREQ_ORIGINAL_RECORD SYSREQ_OUR_FIRM SYSREQ_PROFILING_SETTINGS_BATCH_LOGING SYSREQ_PROFILING_SETTINGS_BATCH_SIZE SYSREQ_PROFILING_SETTINGS_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_SQL_PROFILING_ENABLED SYSREQ_PROFILING_SETTINGS_START_LOGGED SYSREQ_RECORD_STATUS SYSREQ_REF_REQ_FIELD_NAME SYSREQ_REF_REQ_FORMAT SYSREQ_REF_REQ_GENERATED SYSREQ_REF_REQ_LENGTH SYSREQ_REF_REQ_PRECISION SYSREQ_REF_REQ_REFERENCE SYSREQ_REF_REQ_SECTION SYSREQ_REF_REQ_STORED SYSREQ_REF_REQ_TOKENS SYSREQ_REF_REQ_TYPE SYSREQ_REF_REQ_VIEW SYSREQ_REF_TYPE_ACT_CODE SYSREQ_REF_TYPE_ACT_DESCRIPTION SYSREQ_REF_TYPE_ACT_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_ACT_ON_EXECUTE SYSREQ_REF_TYPE_ACT_ON_EXECUTE_EXISTS SYSREQ_REF_TYPE_ACT_SECTION SYSREQ_REF_TYPE_ADD_PARAMS SYSREQ_REF_TYPE_COMMENT SYSREQ_REF_TYPE_COMMON_SETTINGS SYSREQ_REF_TYPE_DISPLAY_REQUISITE_NAME SYSREQ_REF_TYPE_EVENT_TEXT SYSREQ_REF_TYPE_MAIN_LEADING_REF SYSREQ_REF_TYPE_NAME_IN_SINGULAR SYSREQ_REF_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID SYSREQ_REF_TYPE_NAME_LOCALIZE_ID SYSREQ_REF_TYPE_NUMERATION_METHOD SYSREQ_REF_TYPE_REQ_CODE SYSREQ_REF_TYPE_REQ_DESCRIPTION SYSREQ_REF_TYPE_REQ_DESCRIPTION_LOCALIZE_ID SYSREQ_REF_TYPE_REQ_IS_CONTROL SYSREQ_REF_TYPE_REQ_IS_FILTER SYSREQ_REF_TYPE_REQ_IS_LEADING SYSREQ_REF_TYPE_REQ_IS_REQUIRED SYSREQ_REF_TYPE_REQ_NUMBER SYSREQ_REF_TYPE_REQ_ON_CHANGE SYSREQ_REF_TYPE_REQ_ON_CHANGE_EXISTS SYSREQ_REF_TYPE_REQ_ON_SELECT SYSREQ_REF_TYPE_REQ_ON_SELECT_KIND SYSREQ_REF_TYPE_REQ_SECTION SYSREQ_REF_TYPE_VIEW_CARD SYSREQ_REF_TYPE_VIEW_CODE SYSREQ_REF_TYPE_VIEW_COMMENT SYSREQ_REF_TYPE_VIEW_IS_MAIN SYSREQ_REF_TYPE_VIEW_NAME SYSREQ_REF_TYPE_VIEW_NAME_LOCALIZE_ID SYSREQ_REFERENCE_TYPE_ID SYSREQ_STATE SYSREQ_STAT\u0415 SYSREQ_SYSTEM_SETTINGS_VALUE SYSREQ_TYPE SYSREQ_UNIT SYSREQ_UNIT_ID SYSREQ_USER_GROUPS_GROUP_FULL_NAME SYSREQ_USER_GROUPS_GROUP_NAME SYSREQ_USER_GROUPS_GROUP_SERVER_NAME SYSREQ_USERS_ACCESS_RIGHTS SYSREQ_USERS_AUTHENTICATION SYSREQ_USERS_CATEGORY SYSREQ_USERS_COMPONENT SYSREQ_USERS_COMPONENT_USER_IS_PUBLIC SYSREQ_USERS_DOMAIN SYSREQ_USERS_FULL_USER_NAME SYSREQ_USERS_GROUP SYSREQ_USERS_IS_MAIN_SERVER SYSREQ_USERS_LOGIN SYSREQ_USERS_REFERENCE_USER_IS_PUBLIC SYSREQ_USERS_STATUS SYSREQ_USERS_USER_CERTIFICATE SYSREQ_USERS_USER_CERTIFICATE_INFO SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_NAME SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_VERSION SYSREQ_USERS_USER_CERTIFICATE_STATE SYSREQ_USERS_USER_CERTIFICATE_SUBJECT_NAME SYSREQ_USERS_USER_CERTIFICATE_THUMBPRINT SYSREQ_USERS_USER_DEFAULT_CERTIFICATE SYSREQ_USERS_USER_DESCRIPTION SYSREQ_USERS_USER_GLOBAL_NAME SYSREQ_USERS_USER_LOGIN SYSREQ_USERS_USER_MAIN_SERVER SYSREQ_USERS_USER_TYPE SYSREQ_WORK_RULES_FOLDER_ID RESULT_VAR_NAME RESULT_VAR_NAME_ENG AUTO_NUMERATION_RULE_ID CANT_CHANGE_ID_REQUISITE_RULE_ID CANT_CHANGE_OURFIRM_REQUISITE_RULE_ID CHECK_CHANGING_REFERENCE_RECORD_USE_RULE_ID CHECK_CODE_REQUISITE_RULE_ID CHECK_DELETING_REFERENCE_RECORD_USE_RULE_ID CHECK_FILTRATER_CHANGES_RULE_ID CHECK_RECORD_INTERVAL_RULE_ID CHECK_REFERENCE_INTERVAL_RULE_ID CHECK_REQUIRED_DATA_FULLNESS_RULE_ID CHECK_REQUIRED_REQUISITES_FULLNESS_RULE_ID MAKE_RECORD_UNRATIFIED_RULE_ID RESTORE_AUTO_NUMERATION_RULE_ID SET_FIRM_CONTEXT_FROM_RECORD_RULE_ID SET_FIRST_RECORD_IN_LIST_FORM_RULE_ID SET_IDSPS_VALUE_RULE_ID SET_NEXT_CODE_VALUE_RULE_ID SET_OURFIRM_BOUNDS_RULE_ID SET_OURFIRM_REQUISITE_RULE_ID SCRIPT_BLOCK_AFTER_FINISH_EVENT SCRIPT_BLOCK_BEFORE_START_EVENT SCRIPT_BLOCK_EXECUTION_RESULTS_PROPERTY SCRIPT_BLOCK_NAME_PROPERTY SCRIPT_BLOCK_SCRIPT_PROPERTY SUBTASK_BLOCK_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_AFTER_FINISH_EVENT SUBTASK_BLOCK_ASSIGN_PARAMS_EVENT SUBTASK_BLOCK_ATTACHMENTS_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY SUBTASK_BLOCK_BEFORE_START_EVENT SUBTASK_BLOCK_CREATED_TASK_PROPERTY SUBTASK_BLOCK_CREATION_EVENT SUBTASK_BLOCK_DEADLINE_PROPERTY SUBTASK_BLOCK_IMPORTANCE_PROPERTY SUBTASK_BLOCK_INITIATOR_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY SUBTASK_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY SUBTASK_BLOCK_JOBS_TYPE_PROPERTY SUBTASK_BLOCK_NAME_PROPERTY SUBTASK_BLOCK_PARALLEL_ROUTE_PROPERTY SUBTASK_BLOCK_PERFORMERS_PROPERTY SUBTASK_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SUBTASK_BLOCK_REQUIRE_SIGN_PROPERTY SUBTASK_BLOCK_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_START_EVENT SUBTASK_BLOCK_STEP_CONTROL_PROPERTY SUBTASK_BLOCK_SUBJECT_PROPERTY SUBTASK_BLOCK_TASK_CONTROL_PROPERTY SUBTASK_BLOCK_TEXT_PROPERTY SUBTASK_BLOCK_UNLOCK_ATTACHMENTS_ON_STOP_PROPERTY SUBTASK_BLOCK_USE_STANDARD_ROUTE_PROPERTY SUBTASK_BLOCK_WAIT_FOR_TASK_COMPLETE_PROPERTY SYSCOMP_CONTROL_JOBS SYSCOMP_FOLDERS SYSCOMP_JOBS SYSCOMP_NOTICES SYSCOMP_TASKS SYSDLG_CREATE_EDOCUMENT SYSDLG_CREATE_EDOCUMENT_VERSION SYSDLG_CURRENT_PERIOD SYSDLG_EDIT_FUNCTION_HELP SYSDLG_EDOCUMENT_KINDS_FOR_TEMPLATE SYSDLG_EXPORT_MULTIPLE_EDOCUMENTS SYSDLG_EXPORT_SINGLE_EDOCUMENT SYSDLG_IMPORT_EDOCUMENT SYSDLG_MULTIPLE_SELECT SYSDLG_SETUP_ACCESS_RIGHTS SYSDLG_SETUP_DEFAULT_RIGHTS SYSDLG_SETUP_FILTER_CONDITION SYSDLG_SETUP_SIGN_RIGHTS SYSDLG_SETUP_TASK_OBSERVERS SYSDLG_SETUP_TASK_ROUTE SYSDLG_SETUP_USERS_LIST SYSDLG_SIGN_EDOCUMENT SYSDLG_SIGN_MULTIPLE_EDOCUMENTS SYSREF_ACCESS_RIGHTS_TYPES SYSREF_ADMINISTRATION_HISTORY SYSREF_ALL_AVAILABLE_COMPONENTS SYSREF_ALL_AVAILABLE_PRIVILEGES SYSREF_ALL_REPLICATING_COMPONENTS SYSREF_AVAILABLE_DEVELOPERS_COMPONENTS SYSREF_CALENDAR_EVENTS SYSREF_COMPONENT_TOKEN_HISTORY SYSREF_COMPONENT_TOKENS SYSREF_COMPONENTS SYSREF_CONSTANTS SYSREF_DATA_RECEIVE_PROTOCOL SYSREF_DATA_SEND_PROTOCOL SYSREF_DIALOGS SYSREF_DIALOGS_REQUISITES SYSREF_EDITORS SYSREF_EDOC_CARDS SYSREF_EDOC_TYPES SYSREF_EDOCUMENT_CARD_REQUISITES SYSREF_EDOCUMENT_CARD_TYPES SYSREF_EDOCUMENT_CARD_TYPES_REFERENCE SYSREF_EDOCUMENT_CARDS SYSREF_EDOCUMENT_HISTORY SYSREF_EDOCUMENT_KINDS SYSREF_EDOCUMENT_REQUISITES SYSREF_EDOCUMENT_SIGNATURES SYSREF_EDOCUMENT_TEMPLATES SYSREF_EDOCUMENT_TEXT_STORAGES SYSREF_EDOCUMENT_VIEWS SYSREF_FILTERER_SETUP_CONFLICTS SYSREF_FILTRATER_SETTING_CONFLICTS SYSREF_FOLDER_HISTORY SYSREF_FOLDERS SYSREF_FUNCTION_GROUPS SYSREF_FUNCTION_PARAMS SYSREF_FUNCTIONS SYSREF_JOB_HISTORY SYSREF_LINKS SYSREF_LOCALIZATION_DICTIONARY SYSREF_LOCALIZATION_LANGUAGES SYSREF_MODULES SYSREF_PRIVILEGES SYSREF_RECORD_HISTORY SYSREF_REFERENCE_REQUISITES SYSREF_REFERENCE_TYPE_VIEWS SYSREF_REFERENCE_TYPES SYSREF_REFERENCES SYSREF_REFERENCES_REQUISITES SYSREF_REMOTE_SERVERS SYSREF_REPLICATION_SESSIONS_LOG SYSREF_REPLICATION_SESSIONS_PROTOCOL SYSREF_REPORTS SYSREF_ROLES SYSREF_ROUTE_BLOCK_GROUPS SYSREF_ROUTE_BLOCKS SYSREF_SCRIPTS SYSREF_SEARCHES SYSREF_SERVER_EVENTS SYSREF_SERVER_EVENTS_HISTORY SYSREF_STANDARD_ROUTE_GROUPS SYSREF_STANDARD_ROUTES SYSREF_STATUSES SYSREF_SYSTEM_SETTINGS SYSREF_TASK_HISTORY SYSREF_TASK_KIND_GROUPS SYSREF_TASK_KINDS SYSREF_TASK_RIGHTS SYSREF_TASK_SIGNATURES SYSREF_TASKS SYSREF_UNITS SYSREF_USER_GROUPS SYSREF_USER_GROUPS_REFERENCE SYSREF_USER_SUBSTITUTION SYSREF_USERS SYSREF_USERS_REFERENCE SYSREF_VIEWERS SYSREF_WORKING_TIME_CALENDARS ACCESS_RIGHTS_TABLE_NAME EDMS_ACCESS_TABLE_NAME EDOC_TYPES_TABLE_NAME TEST_DEV_DB_NAME TEST_DEV_SYSTEM_CODE TEST_EDMS_DB_NAME TEST_EDMS_MAIN_CODE TEST_EDMS_MAIN_DB_NAME TEST_EDMS_SECOND_CODE TEST_EDMS_SECOND_DB_NAME TEST_EDMS_SYSTEM_CODE TEST_ISB5_MAIN_CODE TEST_ISB5_SECOND_CODE TEST_SQL_SERVER_2005_NAME TEST_SQL_SERVER_NAME ATTENTION_CAPTION cbsCommandLinks cbsDefault CONFIRMATION_CAPTION ERROR_CAPTION INFORMATION_CAPTION mrCancel mrOk EDOC_VERSION_ACTIVE_STAGE_CODE EDOC_VERSION_DESIGN_STAGE_CODE EDOC_VERSION_OBSOLETE_STAGE_CODE cpDataEnciphermentEnabled cpDigitalSignatureEnabled cpID cpIssuer cpPluginVersion cpSerial cpSubjectName cpSubjSimpleName cpValidFromDate cpValidToDate ISBL_SYNTAX NO_SYNTAX XML_SYNTAX WAIT_BLOCK_AFTER_FINISH_EVENT WAIT_BLOCK_BEFORE_START_EVENT WAIT_BLOCK_DEADLINE_PROPERTY WAIT_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY WAIT_BLOCK_NAME_PROPERTY WAIT_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY SYSRES_COMMON SYSRES_CONST SYSRES_MBFUNC SYSRES_SBDATA SYSRES_SBGUI SYSRES_SBINTF SYSRES_SBREFDSC SYSRES_SQLERRORS SYSRES_SYSCOMP atUser atGroup atRole aemEnabledAlways aemDisabledAlways aemEnabledOnBrowse aemEnabledOnEdit aemDisabledOnBrowseEmpty apBegin apEnd alLeft alRight asmNever asmNoButCustomize asmAsLastTime asmYesButCustomize asmAlways cirCommon cirRevoked ctSignature ctEncode ctSignatureEncode clbUnchecked clbChecked clbGrayed ceISB ceAlways ceNever ctDocument ctReference ctScript ctUnknown ctReport ctDialog ctFunction ctFolder ctEDocument ctTask ctJob ctNotice ctControlJob cfInternal cfDisplay ciUnspecified ciWrite ciRead ckFolder ckEDocument ckTask ckJob ckComponentToken ckAny ckReference ckScript ckReport ckDialog ctISBLEditor ctBevel ctButton ctCheckListBox ctComboBox ctComboEdit ctGrid ctDBCheckBox ctDBComboBox ctDBEdit ctDBEllipsis ctDBMemo ctDBNavigator ctDBRadioGroup ctDBStatusLabel ctEdit ctGroupBox ctInplaceHint ctMemo ctPanel ctListBox ctRadioButton ctRichEdit ctTabSheet ctWebBrowser ctImage ctHyperLink ctLabel ctDBMultiEllipsis ctRibbon ctRichView ctInnerPanel ctPanelGroup ctBitButton cctDate cctInteger cctNumeric cctPick cctReference cctString cctText cltInternal cltPrimary cltGUI dseBeforeOpen dseAfterOpen dseBeforeClose dseAfterClose dseOnValidDelete dseBeforeDelete dseAfterDelete dseAfterDeleteOutOfTransaction dseOnDeleteError dseBeforeInsert dseAfterInsert dseOnValidUpdate dseBeforeUpdate dseOnUpdateRatifiedRecord dseAfterUpdate dseAfterUpdateOutOfTransaction dseOnUpdateError dseAfterScroll dseOnOpenRecord dseOnCloseRecord dseBeforeCancel dseAfterCancel dseOnUpdateDeadlockError dseBeforeDetailUpdate dseOnPrepareUpdate dseOnAnyRequisiteChange dssEdit dssInsert dssBrowse dssInActive dftDate dftShortDate dftDateTime dftTimeStamp dotDays dotHours dotMinutes dotSeconds dtkndLocal dtkndUTC arNone arView arEdit arFull ddaView ddaEdit emLock emEdit emSign emExportWithLock emImportWithUnlock emChangeVersionNote emOpenForModify emChangeLifeStage emDelete emCreateVersion emImport emUnlockExportedWithLock emStart emAbort emReInit emMarkAsReaded emMarkAsUnreaded emPerform emAccept emResume emChangeRights emEditRoute emEditObserver emRecoveryFromLocalCopy emChangeWorkAccessType emChangeEncodeTypeToCertificate emChangeEncodeTypeToPassword emChangeEncodeTypeToNone emChangeEncodeTypeToCertificatePassword emChangeStandardRoute emGetText emOpenForView emMoveToStorage emCreateObject emChangeVersionHidden emDeleteVersion emChangeLifeCycleStage emApprovingSign emExport emContinue emLockFromEdit emUnLockForEdit emLockForServer emUnlockFromServer emDelegateAccessRights emReEncode ecotFile ecotProcess eaGet eaCopy eaCreate eaCreateStandardRoute edltAll edltNothing edltQuery essmText essmCard esvtLast esvtLastActive esvtSpecified edsfExecutive edsfArchive edstSQLServer edstFile edvstNone edvstEDocumentVersionCopy edvstFile edvstTemplate edvstScannedFile vsDefault vsDesign vsActive vsObsolete etNone etCertificate etPassword etCertificatePassword ecException ecWarning ecInformation estAll estApprovingOnly evtLast evtLastActive evtQuery fdtString fdtNumeric fdtInteger fdtDate fdtText fdtUnknown fdtWideString fdtLargeInteger ftInbox ftOutbox ftFavorites ftCommonFolder ftUserFolder ftComponents ftQuickLaunch ftShortcuts ftSearch grhAuto grhX1 grhX2 grhX3 hltText hltRTF hltHTML iffBMP iffJPEG iffMultiPageTIFF iffSinglePageTIFF iffTIFF iffPNG im8bGrayscale im24bRGB im1bMonochrome itBMP itJPEG itWMF itPNG ikhInformation ikhWarning ikhError ikhNoIcon icUnknown icScript icFunction icIntegratedReport icAnalyticReport icDataSetEventHandler icActionHandler icFormEventHandler icLookUpEventHandler icRequisiteChangeEventHandler icBeforeSearchEventHandler icRoleCalculation icSelectRouteEventHandler icBlockPropertyCalculation icBlockQueryParamsEventHandler icChangeSearchResultEventHandler icBlockEventHandler icSubTaskInitEventHandler icEDocDataSetEventHandler icEDocLookUpEventHandler icEDocActionHandler icEDocFormEventHandler icEDocRequisiteChangeEventHandler icStructuredConversionRule icStructuredConversionEventBefore icStructuredConversionEventAfter icWizardEventHandler icWizardFinishEventHandler icWizardStepEventHandler icWizardStepFinishEventHandler icWizardActionEnableEventHandler icWizardActionExecuteEventHandler icCreateJobsHandler icCreateNoticesHandler icBeforeLookUpEventHandler icAfterLookUpEventHandler icTaskAbortEventHandler icWorkflowBlockActionHandler icDialogDataSetEventHandler icDialogActionHandler icDialogLookUpEventHandler icDialogRequisiteChangeEventHandler icDialogFormEventHandler icDialogValidCloseEventHandler icBlockFormEventHandler icTaskFormEventHandler icReferenceMethod icEDocMethod icDialogMethod icProcessMessageHandler isShow isHide isByUserSettings jkJob jkNotice jkControlJob jtInner jtLeft jtRight jtFull jtCross lbpAbove lbpBelow lbpLeft lbpRight eltPerConnection eltPerUser sfcUndefined sfcBlack sfcGreen sfcRed sfcBlue sfcOrange sfcLilac sfsItalic sfsStrikeout sfsNormal ldctStandardRoute ldctWizard ldctScript ldctFunction ldctRouteBlock ldctIntegratedReport ldctAnalyticReport ldctReferenceType ldctEDocumentType ldctDialog ldctServerEvents mrcrtNone mrcrtUser mrcrtMaximal mrcrtCustom vtEqual vtGreaterOrEqual vtLessOrEqual vtRange rdYesterday rdToday rdTomorrow rdThisWeek rdThisMonth rdThisYear rdNextMonth rdNextWeek rdLastWeek rdLastMonth rdWindow rdFile rdPrinter rdtString rdtNumeric rdtInteger rdtDate rdtReference rdtAccount rdtText rdtPick rdtUnknown rdtLargeInteger rdtDocument reOnChange reOnChangeValues ttGlobal ttLocal ttUser ttSystem ssmBrowse ssmSelect ssmMultiSelect ssmBrowseModal smSelect smLike smCard stNone stAuthenticating stApproving sctString sctStream sstAnsiSort sstNaturalSort svtEqual svtContain soatString soatNumeric soatInteger soatDatetime soatReferenceRecord soatText soatPick soatBoolean soatEDocument soatAccount soatIntegerCollection soatNumericCollection soatStringCollection soatPickCollection soatDatetimeCollection soatBooleanCollection soatReferenceRecordCollection soatEDocumentCollection soatAccountCollection soatContents soatUnknown tarAbortByUser tarAbortByWorkflowException tvtAllWords tvtExactPhrase tvtAnyWord usNone usCompleted usRedSquare usBlueSquare usYellowSquare usGreenSquare usOrangeSquare usPurpleSquare usFollowUp utUnknown utUser utDeveloper utAdministrator utSystemDeveloper utDisconnected btAnd btDetailAnd btOr btNotOr btOnly vmView vmSelect vmNavigation vsmSingle vsmMultiple vsmMultipleCheck vsmNoSelection wfatPrevious wfatNext wfatCancel wfatFinish wfepUndefined wfepText3 wfepText6 wfepText9 wfepSpinEdit wfepDropDown wfepRadioGroup wfepFlag wfepText12 wfepText15 wfepText18 wfepText21 wfepText24 wfepText27 wfepText30 wfepRadioGroupColumn1 wfepRadioGroupColumn2 wfepRadioGroupColumn3 wfetQueryParameter wfetText wfetDelimiter wfetLabel wptString wptInteger wptNumeric wptBoolean wptDateTime wptPick wptText wptUser wptUserList wptEDocumentInfo wptEDocumentInfoList wptReferenceRecordInfo wptReferenceRecordInfoList wptFolderInfo wptTaskInfo wptContents wptFileName wptDate wsrComplete wsrGoNext wsrGoPrevious wsrCustom wsrCancel wsrGoFinal wstForm wstEDocument wstTaskCard wstReferenceRecordCard wstFinal waAll waPerformers waManual wsbStart wsbFinish wsbNotice wsbStep wsbDecision wsbWait wsbMonitor wsbScript wsbConnector wsbSubTask wsbLifeCycleStage wsbPause wdtInteger wdtFloat wdtString wdtPick wdtDateTime wdtBoolean wdtTask wdtJob wdtFolder wdtEDocument wdtReferenceRecord wdtUser wdtGroup wdtRole wdtIntegerCollection wdtFloatCollection wdtStringCollection wdtPickCollection wdtDateTimeCollection wdtBooleanCollection wdtTaskCollection wdtJobCollection wdtFolderCollection wdtEDocumentCollection wdtReferenceRecordCollection wdtUserCollection wdtGroupCollection wdtRoleCollection wdtContents wdtUserList wdtSearchDescription wdtDeadLine wdtPickSet wdtAccountCollection wiLow wiNormal wiHigh wrtSoft wrtHard wsInit wsRunning wsDone wsControlled wsAborted wsContinued wtmFull wtmFromCurrent wtmOnlyCurrent ",
+class:"AltState Application CallType ComponentTokens CreatedJobs CreatedNotices ControlState DialogResult Dialogs EDocuments EDocumentVersionSource Folders GlobalIDs Job Jobs InputValue LookUpReference LookUpRequisiteNames LookUpSearch Object ParentComponent Processes References Requisite ReportName Reports Result Scripts Searches SelectedAttachments SelectedItems SelectMode Sender ServerEvents ServiceFactory ShiftState SubTask SystemDialogs Tasks Wizard Wizards Work \u0412\u044b\u0437\u043e\u0432\u0421\u043f\u043e\u0441\u043e\u0431 \u0418\u043c\u044f\u041e\u0442\u0447\u0435\u0442\u0430 \u0420\u0435\u043a\u0432\u0417\u043d\u0430\u0447 ",
+literal:"null true false nil "};a={begin:"\\.\\s*"+a.UNDERSCORE_IDENT_RE,keywords:f,relevance:0};var g={className:"type",begin:":[ \\t]*("+"IApplication IAccessRights IAccountRepository IAccountSelectionRestrictions IAction IActionList IAdministrationHistoryDescription IAnchors IApplication IArchiveInfo IAttachment IAttachmentList ICheckListBox ICheckPointedList IColumn IComponent IComponentDescription IComponentToken IComponentTokenFactory IComponentTokenInfo ICompRecordInfo IConnection IContents IControl IControlJob IControlJobInfo IControlList ICrypto ICrypto2 ICustomJob ICustomJobInfo ICustomListBox ICustomObjectWizardStep ICustomWork ICustomWorkInfo IDataSet IDataSetAccessInfo IDataSigner IDateCriterion IDateRequisite IDateRequisiteDescription IDateValue IDeaAccessRights IDeaObjectInfo IDevelopmentComponentLock IDialog IDialogFactory IDialogPickRequisiteItems IDialogsFactory IDICSFactory IDocRequisite IDocumentInfo IDualListDialog IECertificate IECertificateInfo IECertificates IEditControl IEditorForm IEdmsExplorer IEdmsObject IEdmsObjectDescription IEdmsObjectFactory IEdmsObjectInfo IEDocument IEDocumentAccessRights IEDocumentDescription IEDocumentEditor IEDocumentFactory IEDocumentInfo IEDocumentStorage IEDocumentVersion IEDocumentVersionListDialog IEDocumentVersionSource IEDocumentWizardStep IEDocVerSignature IEDocVersionState IEnabledMode IEncodeProvider IEncrypter IEvent IEventList IException IExternalEvents IExternalHandler IFactory IField IFileDialog IFolder IFolderDescription IFolderDialog IFolderFactory IFolderInfo IForEach IForm IFormTitle IFormWizardStep IGlobalIDFactory IGlobalIDInfo IGrid IHasher IHistoryDescription IHyperLinkControl IImageButton IImageControl IInnerPanel IInplaceHint IIntegerCriterion IIntegerList IIntegerRequisite IIntegerValue IISBLEditorForm IJob IJobDescription IJobFactory IJobForm IJobInfo ILabelControl ILargeIntegerCriterion ILargeIntegerRequisite ILargeIntegerValue ILicenseInfo ILifeCycleStage IList IListBox ILocalIDInfo ILocalization ILock IMemoryDataSet IMessagingFactory IMetadataRepository INotice INoticeInfo INumericCriterion INumericRequisite INumericValue IObject IObjectDescription IObjectImporter IObjectInfo IObserver IPanelGroup IPickCriterion IPickProperty IPickRequisite IPickRequisiteDescription IPickRequisiteItem IPickRequisiteItems IPickValue IPrivilege IPrivilegeList IProcess IProcessFactory IProcessMessage IProgress IProperty IPropertyChangeEvent IQuery IReference IReferenceCriterion IReferenceEnabledMode IReferenceFactory IReferenceHistoryDescription IReferenceInfo IReferenceRecordCardWizardStep IReferenceRequisiteDescription IReferencesFactory IReferenceValue IRefRequisite IReport IReportFactory IRequisite IRequisiteDescription IRequisiteDescriptionList IRequisiteFactory IRichEdit IRouteStep IRule IRuleList ISchemeBlock IScript IScriptFactory ISearchCriteria ISearchCriterion ISearchDescription ISearchFactory ISearchFolderInfo ISearchForObjectDescription ISearchResultRestrictions ISecuredContext ISelectDialog IServerEvent IServerEventFactory IServiceDialog IServiceFactory ISignature ISignProvider ISignProvider2 ISignProvider3 ISimpleCriterion IStringCriterion IStringList IStringRequisite IStringRequisiteDescription IStringValue ISystemDialogsFactory ISystemInfo ITabSheet ITask ITaskAbortReasonInfo ITaskCardWizardStep ITaskDescription ITaskFactory ITaskInfo ITaskRoute ITextCriterion ITextRequisite ITextValue ITreeListSelectDialog IUser IUserList IValue IView IWebBrowserControl IWizard IWizardAction IWizardFactory IWizardFormElement IWizardParam IWizardPickParam IWizardReferenceParam IWizardStep IWorkAccessRights IWorkDescription IWorkflowAskableParam IWorkflowAskableParams IWorkflowBlock IWorkflowBlockResult IWorkflowEnabledMode IWorkflowParam IWorkflowPickParam IWorkflowReferenceParam IWorkState IWorkTreeCustomNode IWorkTreeJobNode IWorkTreeTaskNode IXMLEditorForm SBCrypto".replace(/\s/g,
+"|")+")",end:"[ \\t]*=",excludeEnd:!0},k={className:"variable",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",keywords:f,begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",relevance:0,containts:[g,a]};return{aliases:["isbl"],case_insensitive:!0,lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",keywords:f,illegal:"\\$|\\?|%|,|;$|~|#|@|</",
+contains:[{className:"function",begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*\\(",end:"\\)$",returnBegin:!0,lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",keywords:f,illegal:"[\\[\\]\\|\\$\\?%,~#@]",contains:[{className:"title",lexemes:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_!][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*",keywords:{built_in:"AddSubString AdjustLineBreaks AmountInWords Analysis ArrayDimCount ArrayHighBound ArrayLowBound ArrayOf ArrayReDim Assert Assigned BeginOfMonth BeginOfPeriod BuildProfilingOperationAnalysis CallProcedure CanReadFile CArrayElement CDataSetRequisite ChangeDate ChangeReferenceDataset Char CharPos CheckParam CheckParamValue CompareStrings ConstantExists ControlState ConvertDateStr Copy CopyFile CreateArray CreateCachedReference CreateConnection CreateDialog CreateDualListDialog CreateEditor CreateException CreateFile CreateFolderDialog CreateInputDialog CreateLinkFile CreateList CreateLock CreateMemoryDataSet CreateObject CreateOpenDialog CreateProgress CreateQuery CreateReference CreateReport CreateSaveDialog CreateScript CreateSQLPivotFunction CreateStringList CreateTreeListSelectDialog CSelectSQL CSQL CSubString CurrentUserID CurrentUserName CurrentVersion DataSetLocateEx DateDiff DateTimeDiff DateToStr DayOfWeek DeleteFile DirectoryExists DisableCheckAccessRights DisableCheckFullShowingRestriction DisableMassTaskSendingRestrictions DropTable DupeString EditText EnableCheckAccessRights EnableCheckFullShowingRestriction EnableMassTaskSendingRestrictions EndOfMonth EndOfPeriod ExceptionExists ExceptionsOff ExceptionsOn Execute ExecuteProcess Exit ExpandEnvironmentVariables ExtractFileDrive ExtractFileExt ExtractFileName ExtractFilePath ExtractParams FileExists FileSize FindFile FindSubString FirmContext ForceDirectories Format FormatDate FormatNumeric FormatSQLDate FormatString FreeException GetComponent GetComponentLaunchParam GetConstant GetLastException GetReferenceRecord GetRefTypeByRefID GetTableID GetTempFolder IfThen In IndexOf InputDialog InputDialogEx InteractiveMode IsFileLocked IsGraphicFile IsNumeric Length LoadString LoadStringFmt LocalTimeToUTC LowerCase Max MessageBox MessageBoxEx MimeDecodeBinary MimeDecodeString MimeEncodeBinary MimeEncodeString Min MoneyInWords MoveFile NewID Now OpenFile Ord Precision Raise ReadCertificateFromFile ReadFile ReferenceCodeByID ReferenceNumber ReferenceRequisiteMode ReferenceRequisiteValue RegionDateSettings RegionNumberSettings RegionTimeSettings RegRead RegWrite RenameFile Replace Round SelectServerCode SelectSQL ServerDateTime SetConstant SetManagedFolderFieldsState ShowConstantsInputDialog ShowMessage Sleep Split SQL SQL2XLSTAB SQLProfilingSendReport StrToDate SubString SubStringCount SystemSetting Time TimeDiff Today Transliterate Trim UpperCase UserStatus UTCToLocalTime ValidateXML VarIsClear VarIsEmpty VarIsNull WorkTimeDiff WriteFile WriteFileEx WriteObjectHistory \u0410\u043d\u0430\u043b\u0438\u0437 \u0411\u0430\u0437\u0430\u0414\u0430\u043d\u043d\u044b\u0445 \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0418\u043d\u0444\u043e \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0412\u0432\u043e\u0434 \u0412\u0432\u043e\u0434\u041c\u0435\u043d\u044e \u0412\u0435\u0434\u0421 \u0412\u0435\u0434\u0421\u043f\u0440 \u0412\u0435\u0440\u0445\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0412\u043d\u0435\u0448\u041f\u0440\u043e\u0433\u0440 \u0412\u043e\u0441\u0441\u0442 \u0412\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f\u041f\u0430\u043f\u043a\u0430 \u0412\u0440\u0435\u043c\u044f \u0412\u044b\u0431\u043e\u0440SQL \u0412\u044b\u0431\u0440\u0430\u0442\u044c\u0417\u0430\u043f\u0438\u0441\u044c \u0412\u044b\u0434\u0435\u043b\u0438\u0442\u044c\u0421\u0442\u0440 \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0412\u044b\u043f\u041f\u0440\u043e\u0433\u0440 \u0413\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u0438\u0439\u0424\u0430\u0439\u043b \u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f\u0421\u0435\u0440\u0432 \u0414\u0435\u043d\u044c\u041d\u0435\u0434\u0435\u043b\u0438 \u0414\u0438\u0430\u043b\u043e\u0433\u0414\u0430\u041d\u0435\u0442 \u0414\u043b\u0438\u043d\u0430\u0421\u0442\u0440 \u0414\u043e\u0431\u041f\u043e\u0434\u0441\u0442\u0440 \u0415\u041f\u0443\u0441\u0442\u043e \u0415\u0441\u043b\u0438\u0422\u043e \u0415\u0427\u0438\u0441\u043b\u043e \u0417\u0430\u043c\u041f\u043e\u0434\u0441\u0442\u0440 \u0417\u0430\u043f\u0438\u0441\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0430 \u0417\u043d\u0430\u0447\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u0414\u0422\u0438\u043f\u0421\u043f\u0440 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0414\u0438\u0441\u043a \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0418\u043c\u044f\u0424\u0430\u0439\u043b\u0430 \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u041f\u0443\u0442\u044c \u0418\u0437\u0432\u043b\u0435\u0447\u044c\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0418\u0437\u043c\u0414\u0430\u0442 \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0420\u0430\u0437\u043c\u0435\u0440\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u0418\u043c\u044f\u041e\u0440\u0433 \u0418\u043c\u044f\u041f\u043e\u043b\u044f\u0421\u043f\u0440 \u0418\u043d\u0434\u0435\u043a\u0441 \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0428\u0430\u0433 \u0418\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439\u0420\u0435\u0436\u0438\u043c \u0418\u0442\u043e\u0433\u0422\u0431\u043b\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0412\u0435\u0434\u0421\u043f\u0440 \u041a\u043e\u0434\u0412\u0438\u0434\u0421\u043f\u0440\u041f\u043e\u0418\u0414 \u041a\u043e\u0434\u041f\u043eAnalit \u041a\u043e\u0434\u0421\u0438\u043c\u0432\u043e\u043b\u0430 \u041a\u043e\u0434\u0421\u043f\u0440 \u041a\u043e\u043b\u041f\u043e\u0434\u0441\u0442\u0440 \u041a\u043e\u043b\u041f\u0440\u043e\u043f \u041a\u043e\u043d\u041c\u0435\u0441 \u041a\u043e\u043d\u0441\u0442 \u041a\u043e\u043d\u0441\u0442\u0415\u0441\u0442\u044c \u041a\u043e\u043d\u0441\u0442\u0417\u043d\u0430\u0447 \u041a\u043e\u043d\u0422\u0440\u0430\u043d \u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041a\u043e\u043f\u0438\u044f\u0421\u0442\u0440 \u041a\u041f\u0435\u0440\u0438\u043e\u0434 \u041a\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u043a\u0441 \u041c\u0430\u043a\u0441\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u041c\u0430\u0441\u0441\u0438\u0432 \u041c\u0435\u043d\u044e \u041c\u0435\u043d\u044e\u0420\u0430\u0441\u0448 \u041c\u0438\u043d \u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u041d\u0430\u0439\u0442\u0438\u0420\u0430\u0441\u0448 \u041d\u0430\u0438\u043c\u0412\u0438\u0434\u0421\u043f\u0440 \u041d\u0430\u0438\u043c\u041f\u043eAnalit \u041d\u0430\u0438\u043c\u0421\u043f\u0440 \u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c\u041f\u0435\u0440\u0435\u0432\u043e\u0434\u044b\u0421\u0442\u0440\u043e\u043a \u041d\u0430\u0447\u041c\u0435\u0441 \u041d\u0430\u0447\u0422\u0440\u0430\u043d \u041d\u0438\u0436\u043d\u044f\u044f\u0413\u0440\u0430\u043d\u0438\u0446\u0430\u041c\u0430\u0441\u0441\u0438\u0432\u0430 \u041d\u043e\u043c\u0435\u0440\u0421\u043f\u0440 \u041d\u041f\u0435\u0440\u0438\u043e\u0434 \u041e\u043a\u043d\u043e \u041e\u043a\u0440 \u041e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u041e\u0442\u043b\u0418\u043d\u0444\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u041e\u0442\u043b\u0418\u043d\u0444\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u041e\u0442\u0447\u0435\u0442 \u041e\u0442\u0447\u0435\u0442\u0410\u043d\u0430\u043b \u041e\u0442\u0447\u0435\u0442\u0418\u043d\u0442 \u041f\u0430\u043f\u043a\u0430\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u041f\u0430\u0443\u0437\u0430 \u041f\u0412\u044b\u0431\u043e\u0440SQL \u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u041f\u043e\u0434\u0441\u0442\u0440 \u041f\u043e\u0438\u0441\u043a\u0421\u0442\u0440 \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c\u0418\u0414\u0422\u0430\u0431\u043b\u0438\u0446\u044b \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u0414 \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0418\u043c\u044f \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0421\u0442\u0430\u0442\u0443\u0441 \u041f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0417\u043d\u0430\u0447 \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\u0423\u0441\u043b\u043e\u0432\u0438\u0435 \u0420\u0430\u0437\u0431\u0421\u0442\u0440 \u0420\u0430\u0437\u043d\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0414\u0430\u0442 \u0420\u0430\u0437\u043d\u0414\u0430\u0442\u0430\u0412\u0440\u0435\u043c\u044f \u0420\u0430\u0437\u043d\u0420\u0430\u0431\u0412\u0440\u0435\u043c\u044f \u0420\u0435\u0433\u0423\u0441\u0442\u0412\u0440\u0435\u043c \u0420\u0435\u0433\u0423\u0441\u0442\u0414\u0430\u0442 \u0420\u0435\u0433\u0423\u0441\u0442\u0427\u0441\u043b \u0420\u0435\u0434\u0422\u0435\u043a\u0441\u0442 \u0420\u0435\u0435\u0441\u0442\u0440\u0417\u0430\u043f\u0438\u0441\u044c \u0420\u0435\u0435\u0441\u0442\u0440\u0421\u043f\u0438\u0441\u043e\u043a\u0418\u043c\u0435\u043d\u041f\u0430\u0440\u0430\u043c \u0420\u0435\u0435\u0441\u0442\u0440\u0427\u0442\u0435\u043d\u0438\u0435 \u0420\u0435\u043a\u0432\u0421\u043f\u0440 \u0420\u0435\u043a\u0432\u0421\u043f\u0440\u041f\u0440 \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0421\u0435\u0439\u0447\u0430\u0441 \u0421\u0435\u0440\u0432\u0435\u0440 \u0421\u0435\u0440\u0432\u0435\u0440\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u0418\u0414 \u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0421\u0436\u041f\u0440\u043e\u0431 \u0421\u0438\u043c\u0432\u043e\u043b \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0414\u0438\u0440\u0435\u043a\u0442\u0443\u043c\u041a\u043e\u0434 \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0421\u0438\u0441\u0442\u0435\u043c\u0430\u041a\u043e\u0434 \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u0418\u0437\u0414\u0432\u0443\u0445\u0421\u043f\u0438\u0441\u043a\u043e\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0412\u044b\u0431\u043e\u0440\u0430\u041f\u0430\u043f\u043a\u0438 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0414\u0438\u0430\u043b\u043e\u0433\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\u0424\u0430\u0439\u043b\u0430 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0417\u0430\u043f\u0440\u043e\u0441 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041c\u0430\u0441\u0441\u0438\u0432 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0431\u044a\u0435\u043a\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041e\u0442\u0447\u0435\u0442 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u041f\u0430\u043f\u043a\u0443 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0442\u0440\u043e\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043e\u0437\u0434\u0430\u0442\u044c\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u043e\u0437\u0434\u0421\u043f\u0440 \u0421\u043e\u0441\u0442\u0421\u043f\u0440 \u0421\u043e\u0445\u0440 \u0421\u043e\u0445\u0440\u0421\u043f\u0440 \u0421\u043f\u0438\u0441\u043e\u043a\u0421\u0438\u0441\u0442\u0435\u043c \u0421\u043f\u0440 \u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0415\u0441\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0421\u043d\u044f\u0442\u044c\u0420\u0430\u0441\u0448 \u0421\u043f\u0440\u0411\u043b\u043e\u043a\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u0418\u0437\u043c\u041d\u0430\u0431\u0414\u0430\u043d \u0421\u043f\u0440\u041a\u043e\u0434 \u0421\u043f\u0440\u041d\u043e\u043c\u0435\u0440 \u0421\u043f\u0440\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0421\u043f\u0440\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u041f\u0430\u0440\u0430\u043c \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0417\u043d\u0430\u0447 \u0421\u043f\u0440\u041f\u043e\u043b\u0435\u0418\u043c\u044f \u0421\u043f\u0440\u0420\u0435\u043a\u0432 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0412\u0432\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041d\u043e\u0432\u044b\u0435 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440 \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u041f\u0440\u0435\u0434\u0417\u043d \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0420\u0435\u0436\u0438\u043c \u0421\u043f\u0440\u0420\u0435\u043a\u0432\u0422\u0438\u043f\u0422\u0435\u043a\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0421\u043f\u0440\u0421\u043e\u0441\u0442 \u0421\u043f\u0440\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0421\u043f\u0440\u0422\u0431\u043b\u0418\u0442\u043e\u0433 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041a\u043e\u043b \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0430\u043a\u0441 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041c\u0438\u043d \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u041f\u0440\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043b\u0435\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0421\u043e\u0437\u0434 \u0421\u043f\u0440\u0422\u0431\u043b\u0421\u0442\u0440\u0423\u0434 \u0421\u043f\u0440\u0422\u0435\u043a\u041f\u0440\u0435\u0434\u0441\u0442 \u0421\u043f\u0440\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0421\u0440\u0430\u0432\u043d\u0438\u0442\u044c\u0421\u0442\u0440 \u0421\u0442\u0440\u0412\u0435\u0440\u0445\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u041d\u0438\u0436\u043d\u0420\u0435\u0433\u0438\u0441\u0442\u0440 \u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0421\u0443\u043c\u041f\u0440\u043e\u043f \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u041f\u0430\u0440\u0430\u043c \u0422\u0435\u043a\u0412\u0435\u0440\u0441\u0438\u044f \u0422\u0435\u043a\u041e\u0440\u0433 \u0422\u043e\u0447\u043d \u0422\u0440\u0430\u043d \u0422\u0440\u0430\u043d\u0441\u043b\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044f \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0422\u0430\u0431\u043b\u0438\u0446\u0443 \u0423\u0434\u0430\u043b\u0438\u0442\u044c\u0424\u0430\u0439\u043b \u0423\u0434\u0421\u043f\u0440 \u0423\u0434\u0421\u0442\u0440\u0422\u0431\u043b\u0421\u043f\u0440 \u0423\u0441\u0442 \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442 \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f \u0424\u0430\u0439\u043b\u0412\u0440\u0435\u043c\u044f\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0417\u0430\u043d\u044f\u0442 \u0424\u0430\u0439\u043b\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0418\u0441\u043a\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041c\u043e\u0436\u043d\u043e\u0427\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0424\u0430\u0439\u043b\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0424\u0430\u0439\u043b\u0420\u0430\u0437\u043c\u0435\u0440 \u0424\u0430\u0439\u043b\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0441\u044b\u043b\u043a\u0430\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0424\u0430\u0439\u043b\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0424\u0430\u0439\u043b\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0424\u043c\u0442SQL\u0414\u0430\u0442 \u0424\u043c\u0442\u0414\u0430\u0442 \u0424\u043c\u0442\u0421\u0442\u0440 \u0424\u043c\u0442\u0427\u0441\u043b \u0424\u043e\u0440\u043c\u0430\u0442 \u0426\u041c\u0430\u0441\u0441\u0438\u0432\u042d\u043b\u0435\u043c\u0435\u043d\u0442 \u0426\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442 \u0426\u041f\u043e\u0434\u0441\u0442\u0440 "},
+begin:"[A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_][A-Za-z\u0410-\u042f\u0430-\u044f\u0451\u0401_0-9]*\\(",end:"\\(",returnBegin:!0,excludeEnd:!0},a,k,b,c,e]},g,a,k,b,c,e]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",
+illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+
 a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
 contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,
-a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){var b={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
+a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){var c={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
 literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},
-d={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},f={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,d,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:b,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},
-{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>",
-returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:e}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
-{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("jboss-cli",function(a){return{aliases:["wildfly-cli"],lexemes:"[a-z-]+",keywords:{keyword:"alias batch cd clear command connect connection-factory connection-info data-source deploy deployment-info deployment-overlay echo echo-dmr help history if jdbc-driver-info jms-queue|20 jms-topic|20 ls patch pwd quit read-attribute read-operation reload rollout-plan run-batch set shutdown try unalias undeploy unset version xa-data-source",
+b={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:c,contains:[]},f={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,b,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:c,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},
+{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,f,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>",
+returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,contains:e}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
+{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor get set",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("jboss-cli",function(a){return{aliases:["wildfly-cli"],lexemes:"[a-z-]+",keywords:{keyword:"alias batch cd clear command connect connection-factory connection-info data-source deploy deployment-info deployment-overlay echo echo-dmr help history if jdbc-driver-info jms-queue|20 jms-topic|20 ls patch pwd quit read-attribute read-operation reload rollout-plan run-batch set shutdown try unalias undeploy unset version xa-data-source",
 literal:"true false"},contains:[a.HASH_COMMENT_MODE,a.QUOTE_STRING_MODE,{className:"params",begin:/--[\w\-=\/]+/},{className:"function",begin:/:[\w\-.]+/,relevance:0},{className:"string",begin:/\B(([\/.])[\w\-.\/=]+)+/},{className:"params",begin:/\(/,end:/\)/,contains:[{begin:/[\w-]+ *=/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/[\w-]+/}]}],relevance:0}]}});b.registerLanguage("json",function(a){var b={literal:"true false null"},d=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],e={end:",",
 endsWithParent:!0,excludeEnd:!0,contains:d,keywords:b},f={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(e,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(e)],illegal:"\\S"};d.splice(d.length,0,f,a);return{contains:d,keywords:b,illegal:"\\S"}});b.registerLanguage("julia",function(a){var b={keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",
 literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi \u03b3 \u03c0 \u03c6 ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},
@@ -224,7 +243,7 @@
 b,e],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},{className:"string",contains:[a.BACKSLASH_ESCAPE,b,e],begin:"`",end:"`"},{className:"meta",begin:"@[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*"},{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},a.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}];b.contains=d.contains;return d});b.registerLanguage("julia-repl",
 function(a){return{contains:[{className:"meta",begin:/^julia>/,relevance:10,starts:{end:/^(?![ ]{6})/,subLanguage:"julia"},aliases:["jldoctest"]}]}});b.registerLanguage("kotlin",function(a){var b={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",
 built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},d={className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"@"},e={className:"subst",begin:"\\${",end:"}",contains:[a.APOS_STRING_MODE,a.C_NUMBER_MODE]},f={className:"variable",begin:"\\$"+a.UNDERSCORE_IDENT_RE};e={className:"string",variants:[{begin:'"""',end:'"""',contains:[f,e]},{begin:"'",end:"'",illegal:/\n/,contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[a.BACKSLASH_ESCAPE,f,
-e]}]};f={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+a.UNDERSCORE_IDENT_RE+")?"};var g={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]};return{keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"keyword",begin:/\b(break|continue|return|this)\b/,
+e]}]};f={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+a.UNDERSCORE_IDENT_RE+")?"};var g={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]};return{aliases:["kt"],keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"keyword",begin:/\b(break|continue|return|this)\b/,
 starts:{contains:[{className:"symbol",begin:/@\w+/}]}},d,f,g,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:b,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
 endsWithParent:!0,contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,f,g,e,a.C_NUMBER_MODE]},a.C_BLOCK_COMMENT_MODE]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},a.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,excludeEnd:!0,
 relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},f,g]},e,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}});b.registerLanguage("lasso",function(a){var b={literal:"true false none minimal full all void and or not bw nbw ew new cn ncn lt lte gt gte eq neq rx nrx ft",
@@ -234,13 +253,13 @@
 lexemes:"[a-zA-Z_][\\w.]*|&[lg]t;",keywords:b,contains:[{className:"meta",begin:"\\]|\\?>",relevance:0,starts:{end:"\\[|<\\?(lasso(script)?|=)",returnEnd:!0,relevance:0,contains:[d]}},e,f,{className:"meta",begin:"\\[no_square_brackets",starts:{end:"\\[/no_square_brackets\\]",lexemes:"[a-zA-Z_][\\w.]*|&[lg]t;",keywords:b,contains:[{className:"meta",begin:"\\]|\\?>",relevance:0,starts:{end:"\\[noprocess\\]|<\\?(lasso(script)?|=)",returnEnd:!0,contains:[d]}},e,f].concat(a)}},{className:"meta",begin:"\\[",
 relevance:0},{className:"meta",begin:"^#!",end:"lasso9$",relevance:10}].concat(a)}});b.registerLanguage("ldif",function(a){return{contains:[{className:"attribute",begin:"^dn",end:": ",excludeEnd:!0,starts:{end:"$",relevance:0},relevance:10},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,starts:{end:"$",relevance:0}},{className:"literal",begin:"^-",end:"$"},a.HASH_COMMENT_MODE]}});b.registerLanguage("leaf",function(a){return{contains:[{className:"function",begin:"#+[A-Za-z_0-9]*\\(",end:" {",
 returnBegin:!0,excludeEnd:!0,contains:[{className:"keyword",begin:"#+"},{className:"title",begin:"[A-Za-z_][A-Za-z_0-9]*"},{className:"params",begin:"\\(",end:"\\)",endsParent:!0,contains:[{className:"string",begin:'"',end:'"'},{className:"variable",begin:"[A-Za-z_][A-Za-z_0-9]*"}]}]}]}});b.registerLanguage("less",function(a){var b=[],d=[],e=function(a){return{className:"string",begin:"~?"+a+".*?"+a}},f=function(a,b,c){return{className:a,begin:b,relevance:c}},g={begin:"\\(",end:"\\)",contains:d,relevance:0};
-d.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e("'"),e('"'),a.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},f("number","#[0-9A-Fa-f]+\\b"),g,f("variable","@@?[\\w-]+",10),f("variable","@{[\\w-]+}"),f("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});g=d.concat({begin:"{",end:"}",contains:b});var l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(d)};
-e={begin:"([\\w-]+|@{[\\w-]+})\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:"([\\w-]+|@{[\\w-]+})",end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:d}}]};d={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:d,relevance:0}};var k={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],
-starts:{end:"[;}]",returnEnd:!0,contains:g}};f={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:"([\\w-]+|@{[\\w-]+})",end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,l,f("keyword","all\\b"),f("variable","@{[\\w-]+}"),f("selector-tag","([\\w-]+|@{[\\w-]+})%?",0),f("selector-id","#([\\w-]+|@{[\\w-]+})"),f("selector-class","\\.([\\w-]+|@{[\\w-]+})",0),f("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},
-{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:g},{begin:"!important"}]};b.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,k,e,f);return{case_insensitive:!0,illegal:"[=>'/<($\"]",contains:b}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},
-{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},e=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var f={begin:"\\*",end:"\\*"},g={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},l={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",
-relevance:0},k={contains:[d,e,f,g,{begin:"\\(",end:"\\)",contains:["self",b,e,d,l]},l],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},m={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},n={begin:"\\(\\s*",end:"\\)"},
-h={endsWithParent:!0,relevance:0};n.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[k,m,n,b,d,e,a,f,g,{begin:"\\|[^]*?\\|"},l];return{illegal:/\S/,contains:[d,{className:"meta",begin:"^#!",end:"$"},b,e,a,k,m,n,l]}});b.registerLanguage("livecodeserver",function(a){var b={begin:"\\b[gtps][A-Z]+[A-Za-z0-9_\\-]*\\b|\\$_[A-Z]+",relevance:0},d=[a.C_BLOCK_COMMENT_MODE,a.HASH_COMMENT_MODE,
+d.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e("'"),e('"'),a.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},f("number","#[0-9A-Fa-f]+\\b"),g,f("variable","@@?[\\w-]+",10),f("variable","@{[\\w-]+}"),f("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});g=d.concat({begin:"{",end:"}",contains:b});var k={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(d)};
+e={begin:"([\\w-]+|@{[\\w-]+})\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:"([\\w-]+|@{[\\w-]+})",end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:d}}]};d={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:d,relevance:0}};var m={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],
+starts:{end:"[;}]",returnEnd:!0,contains:g}};f={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:"([\\w-]+|@{[\\w-]+})",end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,k,f("keyword","all\\b"),f("variable","@{[\\w-]+}"),f("selector-tag","([\\w-]+|@{[\\w-]+})%?",0),f("selector-id","#([\\w-]+|@{[\\w-]+})"),f("selector-class","\\.([\\w-]+|@{[\\w-]+})",0),f("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},
+{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:g},{begin:"!important"}]};b.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,m,e,f);return{case_insensitive:!0,illegal:"[=>'/<($\"]",contains:b}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},
+{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},e=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var f={begin:"\\*",end:"\\*"},g={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",
+relevance:0},m={contains:[d,e,f,g,{begin:"\\(",end:"\\)",contains:["self",b,e,d,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},p={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},l={begin:"\\(\\s*",end:"\\)"},
+h={endsWithParent:!0,relevance:0};l.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[m,p,l,b,d,e,a,f,g,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[d,{className:"meta",begin:"^#!",end:"$"},b,e,a,m,p,l,k]}});b.registerLanguage("livecodeserver",function(a){var b={begin:"\\b[gtps][A-Z]+[A-Za-z0-9_\\-]*\\b|\\$_[A-Z]+",relevance:0},d=[a.C_BLOCK_COMMENT_MODE,a.HASH_COMMENT_MODE,
 a.COMMENT("--","$"),a.COMMENT("[^:]//","$")],e=a.inherit(a.TITLE_MODE,{variants:[{begin:"\\b_*rig[A-Z]+[A-Za-z0-9_\\-]*"},{begin:"\\b_[a-z0-9\\-]+"}]}),f=a.inherit(a.TITLE_MODE,{begin:"\\b([A-Za-z0-9_\\-]+)\\b"});return{case_insensitive:!1,keywords:{keyword:"$_COOKIE $_FILES $_GET $_GET_BINARY $_GET_RAW $_POST $_POST_BINARY $_POST_RAW $_SESSION $_SERVER codepoint codepoints segment segments codeunit codeunits sentence sentences trueWord trueWords paragraph after byte bytes english the until http forever descending using line real8 with seventh for stdout finally element word words fourth before black ninth sixth characters chars stderr uInt1 uInt1s uInt2 uInt2s stdin string lines relative rel any fifth items from middle mid at else of catch then third it file milliseconds seconds second secs sec int1 int1s int4 int4s internet int2 int2s normal text item last long detailed effective uInt4 uInt4s repeat end repeat URL in try into switch to words https token binfile each tenth as ticks tick system real4 by dateItems without char character ascending eighth whole dateTime numeric short first ftp integer abbreviated abbr abbrev private case while if div mod wrap and or bitAnd bitNot bitOr bitXor among not in a an within contains ends with begins the keys of keys",
 literal:"SIX TEN FORMFEED NINE ZERO NONE SPACE FOUR FALSE COLON CRLF PI COMMA ENDOFFILE EOF EIGHT FIVE QUOTE EMPTY ONE TRUE RETURN CR LINEFEED RIGHT BACKSLASH NULL SEVEN TAB THREE TWO six ten formfeed nine zero none space four false colon crlf pi comma endoffile eof eight five quote empty one true return cr linefeed right backslash null seven tab three two RIVERSION RISTATE FILE_READ_MODE FILE_WRITE_MODE FILE_WRITE_MODE DIR_WRITE_MODE FILE_READ_UMASK FILE_WRITE_UMASK DIR_READ_UMASK DIR_WRITE_UMASK",
 built_in:"put abs acos aliasReference annuity arrayDecode arrayEncode asin atan atan2 average avg avgDev base64Decode base64Encode baseConvert binaryDecode binaryEncode byteOffset byteToNum cachedURL cachedURLs charToNum cipherNames codepointOffset codepointProperty codepointToNum codeunitOffset commandNames compound compress constantNames cos date dateFormat decompress directories diskSpace DNSServers exp exp1 exp2 exp10 extents files flushEvents folders format functionNames geometricMean global globals hasMemory harmonicMean hostAddress hostAddressToName hostName hostNameToAddress isNumber ISOToMac itemOffset keys len length libURLErrorData libUrlFormData libURLftpCommand libURLLastHTTPHeaders libURLLastRHHeaders libUrlMultipartFormAddPart libUrlMultipartFormData libURLVersion lineOffset ln ln1 localNames log log2 log10 longFilePath lower macToISO matchChunk matchText matrixMultiply max md5Digest median merge millisec millisecs millisecond milliseconds min monthNames nativeCharToNum normalizeText num number numToByte numToChar numToCodepoint numToNativeChar offset open openfiles openProcesses openProcessIDs openSockets paragraphOffset paramCount param params peerAddress pendingMessages platform popStdDev populationStandardDeviation populationVariance popVariance processID random randomBytes replaceText result revCreateXMLTree revCreateXMLTreeFromFile revCurrentRecord revCurrentRecordIsFirst revCurrentRecordIsLast revDatabaseColumnCount revDatabaseColumnIsNull revDatabaseColumnLengths revDatabaseColumnNames revDatabaseColumnNamed revDatabaseColumnNumbered revDatabaseColumnTypes revDatabaseConnectResult revDatabaseCursors revDatabaseID revDatabaseTableNames revDatabaseType revDataFromQuery revdb_closeCursor revdb_columnbynumber revdb_columncount revdb_columnisnull revdb_columnlengths revdb_columnnames revdb_columntypes revdb_commit revdb_connect revdb_connections revdb_connectionerr revdb_currentrecord revdb_cursorconnection revdb_cursorerr revdb_cursors revdb_dbtype revdb_disconnect revdb_execute revdb_iseof revdb_isbof revdb_movefirst revdb_movelast revdb_movenext revdb_moveprev revdb_query revdb_querylist revdb_recordcount revdb_rollback revdb_tablenames revGetDatabaseDriverPath revNumberOfRecords revOpenDatabase revOpenDatabases revQueryDatabase revQueryDatabaseBlob revQueryResult revQueryIsAtStart revQueryIsAtEnd revUnixFromMacPath revXMLAttribute revXMLAttributes revXMLAttributeValues revXMLChildContents revXMLChildNames revXMLCreateTreeFromFileWithNamespaces revXMLCreateTreeWithNamespaces revXMLDataFromXPathQuery revXMLEvaluateXPath revXMLFirstChild revXMLMatchingNode revXMLNextSibling revXMLNodeContents revXMLNumberOfChildren revXMLParent revXMLPreviousSibling revXMLRootNode revXMLRPC_CreateRequest revXMLRPC_Documents revXMLRPC_Error revXMLRPC_GetHost revXMLRPC_GetMethod revXMLRPC_GetParam revXMLText revXMLRPC_Execute revXMLRPC_GetParamCount revXMLRPC_GetParamNode revXMLRPC_GetParamType revXMLRPC_GetPath revXMLRPC_GetPort revXMLRPC_GetProtocol revXMLRPC_GetRequest revXMLRPC_GetResponse revXMLRPC_GetSocket revXMLTree revXMLTrees revXMLValidateDTD revZipDescribeItem revZipEnumerateItems revZipOpenArchives round sampVariance sec secs seconds sentenceOffset sha1Digest shell shortFilePath sin specialFolderPath sqrt standardDeviation statRound stdDev sum sysError systemVersion tan tempName textDecode textEncode tick ticks time to tokenOffset toLower toUpper transpose truewordOffset trunc uniDecode uniEncode upper URLDecode URLEncode URLStatus uuid value variableNames variance version waitDepth weekdayNames wordOffset xsltApplyStylesheet xsltApplyStylesheetFromFile xsltLoadStylesheet xsltLoadStylesheetFromFile add breakpoint cancel clear local variable file word line folder directory URL close socket process combine constant convert create new alias folder directory decrypt delete variable word line folder directory URL dispatch divide do encrypt filter get include intersect kill libURLDownloadToFile libURLFollowHttpRedirects libURLftpUpload libURLftpUploadFile libURLresetAll libUrlSetAuthCallback libURLSetCustomHTTPHeaders libUrlSetExpect100 libURLSetFTPListCommand libURLSetFTPMode libURLSetFTPStopTime libURLSetStatusCallback load multiply socket prepare process post seek rel relative read from process rename replace require resetAll resolve revAddXMLNode revAppendXML revCloseCursor revCloseDatabase revCommitDatabase revCopyFile revCopyFolder revCopyXMLNode revDeleteFolder revDeleteXMLNode revDeleteAllXMLTrees revDeleteXMLTree revExecuteSQL revGoURL revInsertXMLNode revMoveFolder revMoveToFirstRecord revMoveToLastRecord revMoveToNextRecord revMoveToPreviousRecord revMoveToRecord revMoveXMLNode revPutIntoXMLNode revRollBackDatabase revSetDatabaseDriverPath revSetXMLAttribute revXMLRPC_AddParam revXMLRPC_DeleteAllDocuments revXMLAddDTD revXMLRPC_Free revXMLRPC_FreeAll revXMLRPC_DeleteDocument revXMLRPC_DeleteParam revXMLRPC_SetHost revXMLRPC_SetMethod revXMLRPC_SetPort revXMLRPC_SetProtocol revXMLRPC_SetSocket revZipAddItemWithData revZipAddItemWithFile revZipAddUncompressedItemWithData revZipAddUncompressedItemWithFile revZipCancel revZipCloseArchive revZipDeleteItem revZipExtractItemToFile revZipExtractItemToVariable revZipSetProgressCallback revZipRenameItem revZipReplaceItemWithData revZipReplaceItemWithFile revZipOpenArchive send set sort split start stop subtract union unload wait write"},
@@ -260,10 +279,9 @@
 contains:d.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:d}].concat(d)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("makefile",function(a){var b={className:"variable",variants:[{begin:"\\$\\("+a.UNDERSCORE_IDENT_RE+"\\)",
 contains:[a.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]};return{aliases:["mk","mak"],keywords:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath",lexemes:/[\w-]+/,contains:[a.HASH_COMMENT_MODE,b,{className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b]},{className:"variable",begin:/\$\([\w-]+\s/,end:/\)/,keywords:{built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"},
 contains:[b]},{begin:"^"+a.UNDERSCORE_IDENT_RE+"\\s*[:+?]?=",illegal:"\\n",returnBegin:!0,contains:[{begin:"^"+a.UNDERSCORE_IDENT_RE,end:"[:+?]?=",excludeEnd:!0}]},{className:"meta",begin:/^\.PHONY:/,end:/$/,keywords:{"meta-keyword":".PHONY"},lexemes:/[\.\w]+/},{className:"section",begin:/^[^\s]+:/,end:/$/,contains:[b]}]}});b.registerLanguage("mathematica",function(a){return{aliases:["mma"],lexemes:"(\\$|\\b)"+a.IDENT_RE+"\\b",keywords:"AbelianGroup Abort AbortKernels AbortProtect Above Abs Absolute AbsoluteCorrelation AbsoluteCorrelationFunction AbsoluteCurrentValue AbsoluteDashing AbsoluteFileName AbsoluteOptions AbsolutePointSize AbsoluteThickness AbsoluteTime AbsoluteTiming AccountingForm Accumulate Accuracy AccuracyGoal ActionDelay ActionMenu ActionMenuBox ActionMenuBoxOptions Active ActiveItem ActiveStyle AcyclicGraphQ AddOnHelpPath AddTo AdjacencyGraph AdjacencyList AdjacencyMatrix AdjustmentBox AdjustmentBoxOptions AdjustTimeSeriesForecast AffineTransform After AiryAi AiryAiPrime AiryAiZero AiryBi AiryBiPrime AiryBiZero AlgebraicIntegerQ AlgebraicNumber AlgebraicNumberDenominator AlgebraicNumberNorm AlgebraicNumberPolynomial AlgebraicNumberTrace AlgebraicRules AlgebraicRulesData Algebraics AlgebraicUnitQ Alignment AlignmentMarker AlignmentPoint All AllowedDimensions AllowGroupClose AllowInlineCells AllowKernelInitialization AllowReverseGroupClose AllowScriptLevelChange AlphaChannel AlternatingGroup AlternativeHypothesis Alternatives AmbientLight Analytic AnchoredSearch And AndersonDarlingTest AngerJ AngleBracket AngularGauge Animate AnimationCycleOffset AnimationCycleRepetitions AnimationDirection AnimationDisplayTime AnimationRate AnimationRepetitions AnimationRunning Animator AnimatorBox AnimatorBoxOptions AnimatorElements Annotation Annuity AnnuityDue Antialiasing Antisymmetric Apart ApartSquareFree Appearance AppearanceElements AppellF1 Append AppendTo Apply ArcCos ArcCosh ArcCot ArcCoth ArcCsc ArcCsch ArcSec ArcSech ArcSin ArcSinDistribution ArcSinh ArcTan ArcTanh Arg ArgMax ArgMin ArgumentCountQ ARIMAProcess ArithmeticGeometricMean ARMAProcess ARProcess Array ArrayComponents ArrayDepth ArrayFlatten ArrayPad ArrayPlot ArrayQ ArrayReshape ArrayRules Arrays Arrow Arrow3DBox ArrowBox Arrowheads AspectRatio AspectRatioFixed Assert Assuming Assumptions AstronomicalData Asynchronous AsynchronousTaskObject AsynchronousTasks AtomQ Attributes AugmentedSymmetricPolynomial AutoAction AutoDelete AutoEvaluateEvents AutoGeneratedPackage AutoIndent AutoIndentSpacings AutoItalicWords AutoloadPath AutoMatch Automatic AutomaticImageSize AutoMultiplicationSymbol AutoNumberFormatting AutoOpenNotebooks AutoOpenPalettes AutorunSequencing AutoScaling AutoScroll AutoSpacing AutoStyleOptions AutoStyleWords Axes AxesEdge AxesLabel AxesOrigin AxesStyle Axis BabyMonsterGroupB Back Background BackgroundTasksSettings Backslash Backsubstitution Backward Band BandpassFilter BandstopFilter BarabasiAlbertGraphDistribution BarChart BarChart3D BarLegend BarlowProschanImportance BarnesG BarOrigin BarSpacing BartlettHannWindow BartlettWindow BaseForm Baseline BaselinePosition BaseStyle BatesDistribution BattleLemarieWavelet Because BeckmannDistribution Beep Before Begin BeginDialogPacket BeginFrontEndInteractionPacket BeginPackage BellB BellY Below BenfordDistribution BeniniDistribution BenktanderGibratDistribution BenktanderWeibullDistribution BernoulliB BernoulliDistribution BernoulliGraphDistribution BernoulliProcess BernsteinBasis BesselFilterModel BesselI BesselJ BesselJZero BesselK BesselY BesselYZero Beta BetaBinomialDistribution BetaDistribution BetaNegativeBinomialDistribution BetaPrimeDistribution BetaRegularized BetweennessCentrality BezierCurve BezierCurve3DBox BezierCurve3DBoxOptions BezierCurveBox BezierCurveBoxOptions BezierFunction BilateralFilter Binarize BinaryFormat BinaryImageQ BinaryRead BinaryReadList BinaryWrite BinCounts BinLists Binomial BinomialDistribution BinomialProcess BinormalDistribution BiorthogonalSplineWavelet BipartiteGraphQ BirnbaumImportance BirnbaumSaundersDistribution BitAnd BitClear BitGet BitLength BitNot BitOr BitSet BitShiftLeft BitShiftRight BitXor Black BlackmanHarrisWindow BlackmanNuttallWindow BlackmanWindow Blank BlankForm BlankNullSequence BlankSequence Blend Block BlockRandom BlomqvistBeta BlomqvistBetaTest Blue Blur BodePlot BohmanWindow Bold Bookmarks Boole BooleanConsecutiveFunction BooleanConvert BooleanCountingFunction BooleanFunction BooleanGraph BooleanMaxterms BooleanMinimize BooleanMinterms Booleans BooleanTable BooleanVariables BorderDimensions BorelTannerDistribution Bottom BottomHatTransform BoundaryStyle Bounds Box BoxBaselineShift BoxData BoxDimensions Boxed Boxes BoxForm BoxFormFormatTypes BoxFrame BoxID BoxMargins BoxMatrix BoxRatios BoxRotation BoxRotationPoint BoxStyle BoxWhiskerChart Bra BracketingBar BraKet BrayCurtisDistance BreadthFirstScan Break Brown BrownForsytheTest BrownianBridgeProcess BrowserCategory BSplineBasis BSplineCurve BSplineCurve3DBox BSplineCurveBox BSplineCurveBoxOptions BSplineFunction BSplineSurface BSplineSurface3DBox BubbleChart BubbleChart3D BubbleScale BubbleSizes BulletGauge BusinessDayQ ButterflyGraph ButterworthFilterModel Button ButtonBar ButtonBox ButtonBoxOptions ButtonCell ButtonContents ButtonData ButtonEvaluator ButtonExpandable ButtonFrame ButtonFunction ButtonMargins ButtonMinHeight ButtonNote ButtonNotebook ButtonSource ButtonStyle ButtonStyleMenuListing Byte ByteCount ByteOrdering C CachedValue CacheGraphics CalendarData CalendarType CallPacket CanberraDistance Cancel CancelButton CandlestickChart Cap CapForm CapitalDifferentialD CardinalBSplineBasis CarmichaelLambda Cases Cashflow Casoratian Catalan CatalanNumber Catch CauchyDistribution CauchyWindow CayleyGraph CDF CDFDeploy CDFInformation CDFWavelet Ceiling Cell CellAutoOverwrite CellBaseline CellBoundingBox CellBracketOptions CellChangeTimes CellContents CellContext CellDingbat CellDynamicExpression CellEditDuplicate CellElementsBoundingBox CellElementSpacings CellEpilog CellEvaluationDuplicate CellEvaluationFunction CellEventActions CellFrame CellFrameColor CellFrameLabelMargins CellFrameLabels CellFrameMargins CellGroup CellGroupData CellGrouping CellGroupingRules CellHorizontalScrolling CellID CellLabel CellLabelAutoDelete CellLabelMargins CellLabelPositioning CellMargins CellObject CellOpen CellPrint CellProlog Cells CellSize CellStyle CellTags CellularAutomaton CensoredDistribution Censoring Center CenterDot CentralMoment CentralMomentGeneratingFunction CForm ChampernowneNumber ChanVeseBinarize Character CharacterEncoding CharacterEncodingsPath CharacteristicFunction CharacteristicPolynomial CharacterRange Characters ChartBaseStyle ChartElementData ChartElementDataFunction ChartElementFunction ChartElements ChartLabels ChartLayout ChartLegends ChartStyle Chebyshev1FilterModel Chebyshev2FilterModel ChebyshevDistance ChebyshevT ChebyshevU Check CheckAbort CheckAll Checkbox CheckboxBar CheckboxBox CheckboxBoxOptions ChemicalData ChessboardDistance ChiDistribution ChineseRemainder ChiSquareDistribution ChoiceButtons ChoiceDialog CholeskyDecomposition Chop Circle CircleBox CircleDot CircleMinus CirclePlus CircleTimes CirculantGraph CityData Clear ClearAll ClearAttributes ClearSystemCache ClebschGordan ClickPane Clip ClipboardNotebook ClipFill ClippingStyle ClipPlanes ClipRange Clock ClockGauge ClockwiseContourIntegral Close Closed CloseKernels ClosenessCentrality Closing ClosingAutoSave ClosingEvent ClusteringComponents CMYKColor Coarse Coefficient CoefficientArrays CoefficientDomain CoefficientList CoefficientRules CoifletWavelet Collect Colon ColonForm ColorCombine ColorConvert ColorData ColorDataFunction ColorFunction ColorFunctionScaling Colorize ColorNegate ColorOutput ColorProfileData ColorQuantize ColorReplace ColorRules ColorSelectorSettings ColorSeparate ColorSetter ColorSetterBox ColorSetterBoxOptions ColorSlider ColorSpace Column ColumnAlignments ColumnBackgrounds ColumnForm ColumnLines ColumnsEqual ColumnSpacings ColumnWidths CommonDefaultFormatTypes Commonest CommonestFilter CommonUnits CommunityBoundaryStyle CommunityGraphPlot CommunityLabels CommunityRegionStyle CompatibleUnitQ CompilationOptions CompilationTarget Compile Compiled CompiledFunction Complement CompleteGraph CompleteGraphQ CompleteKaryTree CompletionsListPacket Complex Complexes ComplexExpand ComplexInfinity ComplexityFunction ComponentMeasurements ComponentwiseContextMenu Compose ComposeList ComposeSeries Composition CompoundExpression CompoundPoissonDistribution CompoundPoissonProcess CompoundRenewalProcess Compress CompressedData Condition ConditionalExpression Conditioned Cone ConeBox ConfidenceLevel ConfidenceRange ConfidenceTransform ConfigurationPath Congruent Conjugate ConjugateTranspose Conjunction Connect ConnectedComponents ConnectedGraphQ ConnesWindow ConoverTest ConsoleMessage ConsoleMessagePacket ConsolePrint Constant ConstantArray Constants ConstrainedMax ConstrainedMin ContentPadding ContentsBoundingBox ContentSelectable ContentSize Context ContextMenu Contexts ContextToFilename ContextToFileName Continuation Continue ContinuedFraction ContinuedFractionK ContinuousAction ContinuousMarkovProcess ContinuousTimeModelQ ContinuousWaveletData ContinuousWaveletTransform ContourDetect ContourGraphics ContourIntegral ContourLabels ContourLines ContourPlot ContourPlot3D Contours ContourShading ContourSmoothing ContourStyle ContraharmonicMean Control ControlActive ControlAlignment ControllabilityGramian ControllabilityMatrix ControllableDecomposition ControllableModelQ ControllerDuration ControllerInformation ControllerInformationData ControllerLinking ControllerManipulate ControllerMethod ControllerPath ControllerState ControlPlacement ControlsRendering ControlType Convergents ConversionOptions ConversionRules ConvertToBitmapPacket ConvertToPostScript ConvertToPostScriptPacket Convolve ConwayGroupCo1 ConwayGroupCo2 ConwayGroupCo3 CoordinateChartData CoordinatesToolOptions CoordinateTransform CoordinateTransformData CoprimeQ Coproduct CopulaDistribution Copyable CopyDirectory CopyFile CopyTag CopyToClipboard CornerFilter CornerNeighbors Correlation CorrelationDistance CorrelationFunction CorrelationTest Cos Cosh CoshIntegral CosineDistance CosineWindow CosIntegral Cot Coth Count CounterAssignments CounterBox CounterBoxOptions CounterClockwiseContourIntegral CounterEvaluator CounterFunction CounterIncrements CounterStyle CounterStyleMenuListing CountRoots CountryData Covariance CovarianceEstimatorFunction CovarianceFunction CoxianDistribution CoxIngersollRossProcess CoxModel CoxModelFit CramerVonMisesTest CreateArchive CreateDialog CreateDirectory CreateDocument CreateIntermediateDirectories CreatePalette CreatePalettePacket CreateScheduledTask CreateTemporary CreateWindow CriticalityFailureImportance CriticalitySuccessImportance CriticalSection Cross CrossingDetect CrossMatrix Csc Csch CubeRoot Cubics Cuboid CuboidBox Cumulant CumulantGeneratingFunction Cup CupCap Curl CurlyDoubleQuote CurlyQuote CurrentImage CurrentlySpeakingPacket CurrentValue CurvatureFlowFilter CurveClosed Cyan CycleGraph CycleIndexPolynomial Cycles CyclicGroup Cyclotomic Cylinder CylinderBox CylindricalDecomposition D DagumDistribution DamerauLevenshteinDistance DampingFactor Darker Dashed Dashing DataCompression DataDistribution DataRange DataReversed Date DateDelimiters DateDifference DateFunction DateList DateListLogPlot DateListPlot DatePattern DatePlus DateRange DateString DateTicksFormat DaubechiesWavelet DavisDistribution DawsonF DayCount DayCountConvention DayMatchQ DayName DayPlus DayRange DayRound DeBruijnGraph Debug DebugTag Decimal DeclareKnownSymbols DeclarePackage Decompose Decrement DedekindEta Default DefaultAxesStyle DefaultBaseStyle DefaultBoxStyle DefaultButton DefaultColor DefaultControlPlacement DefaultDuplicateCellStyle DefaultDuration DefaultElement DefaultFaceGridsStyle DefaultFieldHintStyle DefaultFont DefaultFontProperties DefaultFormatType DefaultFormatTypeForStyle DefaultFrameStyle DefaultFrameTicksStyle DefaultGridLinesStyle DefaultInlineFormatType DefaultInputFormatType DefaultLabelStyle DefaultMenuStyle DefaultNaturalLanguage DefaultNewCellStyle DefaultNewInlineCellStyle DefaultNotebook DefaultOptions DefaultOutputFormatType DefaultStyle DefaultStyleDefinitions DefaultTextFormatType DefaultTextInlineFormatType DefaultTicksStyle DefaultTooltipStyle DefaultValues Defer DefineExternal DefineInputStreamMethod DefineOutputStreamMethod Definition Degree DegreeCentrality DegreeGraphDistribution DegreeLexicographic DegreeReverseLexicographic Deinitialization Del Deletable Delete DeleteBorderComponents DeleteCases DeleteContents DeleteDirectory DeleteDuplicates DeleteFile DeleteSmallComponents DeleteWithContents DeletionWarning Delimiter DelimiterFlashTime DelimiterMatching Delimiters Denominator DensityGraphics DensityHistogram DensityPlot DependentVariables Deploy Deployed Depth DepthFirstScan Derivative DerivativeFilter DescriptorStateSpace DesignMatrix Det DGaussianWavelet DiacriticalPositioning Diagonal DiagonalMatrix Dialog DialogIndent DialogInput DialogLevel DialogNotebook DialogProlog DialogReturn DialogSymbols Diamond DiamondMatrix DiceDissimilarity DictionaryLookup DifferenceDelta DifferenceOrder DifferenceRoot DifferenceRootReduce Differences DifferentialD DifferentialRoot DifferentialRootReduce DifferentiatorFilter DigitBlock DigitBlockMinimum DigitCharacter DigitCount DigitQ DihedralGroup Dilation Dimensions DiracComb DiracDelta DirectedEdge DirectedEdges DirectedGraph DirectedGraphQ DirectedInfinity Direction Directive Directory DirectoryName DirectoryQ DirectoryStack DirichletCharacter DirichletConvolve DirichletDistribution DirichletL DirichletTransform DirichletWindow DisableConsolePrintPacket DiscreteChirpZTransform DiscreteConvolve DiscreteDelta DiscreteHadamardTransform DiscreteIndicator DiscreteLQEstimatorGains DiscreteLQRegulatorGains DiscreteLyapunovSolve DiscreteMarkovProcess DiscretePlot DiscretePlot3D DiscreteRatio DiscreteRiccatiSolve DiscreteShift DiscreteTimeModelQ DiscreteUniformDistribution DiscreteVariables DiscreteWaveletData DiscreteWaveletPacketTransform DiscreteWaveletTransform Discriminant Disjunction Disk DiskBox DiskMatrix Dispatch DispersionEstimatorFunction Display DisplayAllSteps DisplayEndPacket DisplayFlushImagePacket DisplayForm DisplayFunction DisplayPacket DisplayRules DisplaySetSizePacket DisplayString DisplayTemporary DisplayWith DisplayWithRef DisplayWithVariable DistanceFunction DistanceTransform Distribute Distributed DistributedContexts DistributeDefinitions DistributionChart DistributionDomain DistributionFitTest DistributionParameterAssumptions DistributionParameterQ Dithering Div Divergence Divide DivideBy Dividers Divisible Divisors DivisorSigma DivisorSum DMSList DMSString Do DockedCells DocumentNotebook DominantColors DOSTextFormat Dot DotDashed DotEqual Dotted DoubleBracketingBar DoubleContourIntegral DoubleDownArrow DoubleLeftArrow DoubleLeftRightArrow DoubleLeftTee DoubleLongLeftArrow DoubleLongLeftRightArrow DoubleLongRightArrow DoubleRightArrow DoubleRightTee DoubleUpArrow DoubleUpDownArrow DoubleVerticalBar DoublyInfinite Down DownArrow DownArrowBar DownArrowUpArrow DownLeftRightVector DownLeftTeeVector DownLeftVector DownLeftVectorBar DownRightTeeVector DownRightVector DownRightVectorBar Downsample DownTee DownTeeArrow DownValues DragAndDrop DrawEdges DrawFrontFaces DrawHighlighted Drop DSolve Dt DualLinearProgramming DualSystemsModel DumpGet DumpSave DuplicateFreeQ Dynamic DynamicBox DynamicBoxOptions DynamicEvaluationTimeout DynamicLocation DynamicModule DynamicModuleBox DynamicModuleBoxOptions DynamicModuleParent DynamicModuleValues DynamicName DynamicNamespace DynamicReference DynamicSetting DynamicUpdating DynamicWrapper DynamicWrapperBox DynamicWrapperBoxOptions E EccentricityCentrality EdgeAdd EdgeBetweennessCentrality EdgeCapacity EdgeCapForm EdgeColor EdgeConnectivity EdgeCost EdgeCount EdgeCoverQ EdgeDashing EdgeDelete EdgeDetect EdgeForm EdgeIndex EdgeJoinForm EdgeLabeling EdgeLabels EdgeLabelStyle EdgeList EdgeOpacity EdgeQ EdgeRenderingFunction EdgeRules EdgeShapeFunction EdgeStyle EdgeThickness EdgeWeight Editable EditButtonSettings EditCellTagsSettings EditDistance EffectiveInterest Eigensystem Eigenvalues EigenvectorCentrality Eigenvectors Element ElementData Eliminate EliminationOrder EllipticE EllipticExp EllipticExpPrime EllipticF EllipticFilterModel EllipticK EllipticLog EllipticNomeQ EllipticPi EllipticReducedHalfPeriods EllipticTheta EllipticThetaPrime EmitSound EmphasizeSyntaxErrors EmpiricalDistribution Empty EmptyGraphQ EnableConsolePrintPacket Enabled Encode End EndAdd EndDialogPacket EndFrontEndInteractionPacket EndOfFile EndOfLine EndOfString EndPackage EngineeringForm Enter EnterExpressionPacket EnterTextPacket Entropy EntropyFilter Environment Epilog Equal EqualColumns EqualRows EqualTilde EquatedTo Equilibrium EquirippleFilterKernel Equivalent Erf Erfc Erfi ErlangB ErlangC ErlangDistribution Erosion ErrorBox ErrorBoxOptions ErrorNorm ErrorPacket ErrorsDialogSettings EstimatedDistribution EstimatedProcess EstimatorGains EstimatorRegulator EuclideanDistance EulerE EulerGamma EulerianGraphQ EulerPhi Evaluatable Evaluate Evaluated EvaluatePacket EvaluationCell EvaluationCompletionAction EvaluationElements EvaluationMode EvaluationMonitor EvaluationNotebook EvaluationObject EvaluationOrder Evaluator EvaluatorNames EvenQ EventData EventEvaluator EventHandler EventHandlerTag EventLabels ExactBlackmanWindow ExactNumberQ ExactRootIsolation ExampleData Except ExcludedForms ExcludePods Exclusions ExclusionsStyle Exists Exit ExitDialog Exp Expand ExpandAll ExpandDenominator ExpandFileName ExpandNumerator Expectation ExpectationE ExpectedValue ExpGammaDistribution ExpIntegralE ExpIntegralEi Exponent ExponentFunction ExponentialDistribution ExponentialFamily ExponentialGeneratingFunction ExponentialMovingAverage ExponentialPowerDistribution ExponentPosition ExponentStep Export ExportAutoReplacements ExportPacket ExportString Expression ExpressionCell ExpressionPacket ExpToTrig ExtendedGCD Extension ExtentElementFunction ExtentMarkers ExtentSize ExternalCall ExternalDataCharacterEncoding Extract ExtractArchive ExtremeValueDistribution FaceForm FaceGrids FaceGridsStyle Factor FactorComplete Factorial Factorial2 FactorialMoment FactorialMomentGeneratingFunction FactorialPower FactorInteger FactorList FactorSquareFree FactorSquareFreeList FactorTerms FactorTermsList Fail FailureDistribution False FARIMAProcess FEDisableConsolePrintPacket FeedbackSector FeedbackSectorStyle FeedbackType FEEnableConsolePrintPacket Fibonacci FieldHint FieldHintStyle FieldMasked FieldSize File FileBaseName FileByteCount FileDate FileExistsQ FileExtension FileFormat FileHash FileInformation FileName FileNameDepth FileNameDialogSettings FileNameDrop FileNameJoin FileNames FileNameSetter FileNameSplit FileNameTake FilePrint FileType FilledCurve FilledCurveBox Filling FillingStyle FillingTransform FilterRules FinancialBond FinancialData FinancialDerivative FinancialIndicator Find FindArgMax FindArgMin FindClique FindClusters FindCurvePath FindDistributionParameters FindDivisions FindEdgeCover FindEdgeCut FindEulerianCycle FindFaces FindFile FindFit FindGeneratingFunction FindGeoLocation FindGeometricTransform FindGraphCommunities FindGraphIsomorphism FindGraphPartition FindHamiltonianCycle FindIndependentEdgeSet FindIndependentVertexSet FindInstance FindIntegerNullVector FindKClan FindKClique FindKClub FindKPlex FindLibrary FindLinearRecurrence FindList FindMaximum FindMaximumFlow FindMaxValue FindMinimum FindMinimumCostFlow FindMinimumCut FindMinValue FindPermutation FindPostmanTour FindProcessParameters FindRoot FindSequenceFunction FindSettings FindShortestPath FindShortestTour FindThreshold FindVertexCover FindVertexCut Fine FinishDynamic FiniteAbelianGroupCount FiniteGroupCount FiniteGroupData First FirstPassageTimeDistribution FischerGroupFi22 FischerGroupFi23 FischerGroupFi24Prime FisherHypergeometricDistribution FisherRatioTest FisherZDistribution Fit FitAll FittedModel FixedPoint FixedPointList FlashSelection Flat Flatten FlattenAt FlatTopWindow FlipView Floor FlushPrintOutputPacket Fold FoldList Font FontColor FontFamily FontForm FontName FontOpacity FontPostScriptName FontProperties FontReencoding FontSize FontSlant FontSubstitutions FontTracking FontVariations FontWeight For ForAll Format FormatRules FormatType FormatTypeAutoConvert FormatValues FormBox FormBoxOptions FortranForm Forward ForwardBackward Fourier FourierCoefficient FourierCosCoefficient FourierCosSeries FourierCosTransform FourierDCT FourierDCTFilter FourierDCTMatrix FourierDST FourierDSTMatrix FourierMatrix FourierParameters FourierSequenceTransform FourierSeries FourierSinCoefficient FourierSinSeries FourierSinTransform FourierTransform FourierTrigSeries FractionalBrownianMotionProcess FractionalPart FractionBox FractionBoxOptions FractionLine Frame FrameBox FrameBoxOptions Framed FrameInset FrameLabel Frameless FrameMargins FrameStyle FrameTicks FrameTicksStyle FRatioDistribution FrechetDistribution FreeQ FrequencySamplingFilterKernel FresnelC FresnelS Friday FrobeniusNumber FrobeniusSolve FromCharacterCode FromCoefficientRules FromContinuedFraction FromDate FromDigits FromDMS Front FrontEndDynamicExpression FrontEndEventActions FrontEndExecute FrontEndObject FrontEndResource FrontEndResourceString FrontEndStackSize FrontEndToken FrontEndTokenExecute FrontEndValueCache FrontEndVersion FrontFaceColor FrontFaceOpacity Full FullAxes FullDefinition FullForm FullGraphics FullOptions FullSimplify Function FunctionExpand FunctionInterpolation FunctionSpace FussellVeselyImportance GaborFilter GaborMatrix GaborWavelet GainMargins GainPhaseMargins Gamma GammaDistribution GammaRegularized GapPenalty Gather GatherBy GaugeFaceElementFunction GaugeFaceStyle GaugeFrameElementFunction GaugeFrameSize GaugeFrameStyle GaugeLabels GaugeMarkers GaugeStyle GaussianFilter GaussianIntegers GaussianMatrix GaussianWindow GCD GegenbauerC General GeneralizedLinearModelFit GenerateConditions GeneratedCell GeneratedParameters GeneratingFunction Generic GenericCylindricalDecomposition GenomeData GenomeLookup GeodesicClosing GeodesicDilation GeodesicErosion GeodesicOpening GeoDestination GeodesyData GeoDirection GeoDistance GeoGridPosition GeometricBrownianMotionProcess GeometricDistribution GeometricMean GeometricMeanFilter GeometricTransformation GeometricTransformation3DBox GeometricTransformation3DBoxOptions GeometricTransformationBox GeometricTransformationBoxOptions GeoPosition GeoPositionENU GeoPositionXYZ GeoProjectionData GestureHandler GestureHandlerTag Get GetBoundingBoxSizePacket GetContext GetEnvironment GetFileName GetFrontEndOptionsDataPacket GetLinebreakInformationPacket GetMenusPacket GetPageBreakInformationPacket Glaisher GlobalClusteringCoefficient GlobalPreferences GlobalSession Glow GoldenRatio GompertzMakehamDistribution GoodmanKruskalGamma GoodmanKruskalGammaTest Goto Grad Gradient GradientFilter GradientOrientationFilter Graph GraphAssortativity GraphCenter GraphComplement GraphData GraphDensity GraphDiameter GraphDifference GraphDisjointUnion GraphDistance GraphDistanceMatrix GraphElementData GraphEmbedding GraphHighlight GraphHighlightStyle GraphHub Graphics Graphics3D Graphics3DBox Graphics3DBoxOptions GraphicsArray GraphicsBaseline GraphicsBox GraphicsBoxOptions GraphicsColor GraphicsColumn GraphicsComplex GraphicsComplex3DBox GraphicsComplex3DBoxOptions GraphicsComplexBox GraphicsComplexBoxOptions GraphicsContents GraphicsData GraphicsGrid GraphicsGridBox GraphicsGroup GraphicsGroup3DBox GraphicsGroup3DBoxOptions GraphicsGroupBox GraphicsGroupBoxOptions GraphicsGrouping GraphicsHighlightColor GraphicsRow GraphicsSpacing GraphicsStyle GraphIntersection GraphLayout GraphLinkEfficiency GraphPeriphery GraphPlot GraphPlot3D GraphPower GraphPropertyDistribution GraphQ GraphRadius GraphReciprocity GraphRoot GraphStyle GraphUnion Gray GrayLevel GreatCircleDistance Greater GreaterEqual GreaterEqualLess GreaterFullEqual GreaterGreater GreaterLess GreaterSlantEqual GreaterTilde Green Grid GridBaseline GridBox GridBoxAlignment GridBoxBackground GridBoxDividers GridBoxFrame GridBoxItemSize GridBoxItemStyle GridBoxOptions GridBoxSpacings GridCreationSettings GridDefaultElement GridElementStyleOptions GridFrame GridFrameMargins GridGraph GridLines GridLinesStyle GroebnerBasis GroupActionBase GroupCentralizer GroupElementFromWord GroupElementPosition GroupElementQ GroupElements GroupElementToWord GroupGenerators GroupMultiplicationTable GroupOrbits GroupOrder GroupPageBreakWithin GroupSetwiseStabilizer GroupStabilizer GroupStabilizerChain Gudermannian GumbelDistribution HaarWavelet HadamardMatrix HalfNormalDistribution HamiltonianGraphQ HammingDistance HammingWindow HankelH1 HankelH2 HankelMatrix HannPoissonWindow HannWindow HaradaNortonGroupHN HararyGraph HarmonicMean HarmonicMeanFilter HarmonicNumber Hash HashTable Haversine HazardFunction Head HeadCompose Heads HeavisideLambda HeavisidePi HeavisideTheta HeldGroupHe HeldPart HelpBrowserLookup HelpBrowserNotebook HelpBrowserSettings HermiteDecomposition HermiteH HermitianMatrixQ HessenbergDecomposition Hessian HexadecimalCharacter Hexahedron HexahedronBox HexahedronBoxOptions HiddenSurface HighlightGraph HighlightImage HighpassFilter HigmanSimsGroupHS HilbertFilter HilbertMatrix Histogram Histogram3D HistogramDistribution HistogramList HistogramTransform HistogramTransformInterpolation HitMissTransform HITSCentrality HodgeDual HoeffdingD HoeffdingDTest Hold HoldAll HoldAllComplete HoldComplete HoldFirst HoldForm HoldPattern HoldRest HolidayCalendar HomeDirectory HomePage Horizontal HorizontalForm HorizontalGauge HorizontalScrollPosition HornerForm HotellingTSquareDistribution HoytDistribution HTMLSave Hue HumpDownHump HumpEqual HurwitzLerchPhi HurwitzZeta HyperbolicDistribution HypercubeGraph HyperexponentialDistribution Hyperfactorial Hypergeometric0F1 Hypergeometric0F1Regularized Hypergeometric1F1 Hypergeometric1F1Regularized Hypergeometric2F1 Hypergeometric2F1Regularized HypergeometricDistribution HypergeometricPFQ HypergeometricPFQRegularized HypergeometricU Hyperlink HyperlinkCreationSettings Hyphenation HyphenationOptions HypoexponentialDistribution HypothesisTestData I Identity IdentityMatrix If IgnoreCase Im Image Image3D Image3DSlices ImageAccumulate ImageAdd ImageAdjust ImageAlign ImageApply ImageAspectRatio ImageAssemble ImageCache ImageCacheValid ImageCapture ImageChannels ImageClip ImageColorSpace ImageCompose ImageConvolve ImageCooccurrence ImageCorners ImageCorrelate ImageCorrespondingPoints ImageCrop ImageData ImageDataPacket ImageDeconvolve ImageDemosaic ImageDifference ImageDimensions ImageDistance ImageEffect ImageFeatureTrack ImageFileApply ImageFileFilter ImageFileScan ImageFilter ImageForestingComponents ImageForwardTransformation ImageHistogram ImageKeypoints ImageLevels ImageLines ImageMargins ImageMarkers ImageMeasurements ImageMultiply ImageOffset ImagePad ImagePadding ImagePartition ImagePeriodogram ImagePerspectiveTransformation ImageQ ImageRangeCache ImageReflect ImageRegion ImageResize ImageResolution ImageRotate ImageRotated ImageScaled ImageScan ImageSize ImageSizeAction ImageSizeCache ImageSizeMultipliers ImageSizeRaw ImageSubtract ImageTake ImageTransformation ImageTrim ImageType ImageValue ImageValuePositions Implies Import ImportAutoReplacements ImportString ImprovementImportance In IncidenceGraph IncidenceList IncidenceMatrix IncludeConstantBasis IncludeFileExtension IncludePods IncludeSingularTerm Increment Indent IndentingNewlineSpacings IndentMaxFraction IndependenceTest IndependentEdgeSetQ IndependentUnit IndependentVertexSetQ Indeterminate IndexCreationOptions Indexed IndexGraph IndexTag Inequality InexactNumberQ InexactNumbers Infinity Infix Information Inherited InheritScope Initialization InitializationCell InitializationCellEvaluation InitializationCellWarning InlineCounterAssignments InlineCounterIncrements InlineRules Inner Inpaint Input InputAliases InputAssumptions InputAutoReplacements InputField InputFieldBox InputFieldBoxOptions InputForm InputGrouping InputNamePacket InputNotebook InputPacket InputSettings InputStream InputString InputStringPacket InputToBoxFormPacket Insert InsertionPointObject InsertResults Inset Inset3DBox Inset3DBoxOptions InsetBox InsetBoxOptions Install InstallService InString Integer IntegerDigits IntegerExponent IntegerLength IntegerPart IntegerPartitions IntegerQ Integers IntegerString Integral Integrate Interactive InteractiveTradingChart Interlaced Interleaving InternallyBalancedDecomposition InterpolatingFunction InterpolatingPolynomial Interpolation InterpolationOrder InterpolationPoints InterpolationPrecision Interpretation InterpretationBox InterpretationBoxOptions InterpretationFunction InterpretTemplate InterquartileRange Interrupt InterruptSettings Intersection Interval IntervalIntersection IntervalMemberQ IntervalUnion Inverse InverseBetaRegularized InverseCDF InverseChiSquareDistribution InverseContinuousWaveletTransform InverseDistanceTransform InverseEllipticNomeQ InverseErf InverseErfc InverseFourier InverseFourierCosTransform InverseFourierSequenceTransform InverseFourierSinTransform InverseFourierTransform InverseFunction InverseFunctions InverseGammaDistribution InverseGammaRegularized InverseGaussianDistribution InverseGudermannian InverseHaversine InverseJacobiCD InverseJacobiCN InverseJacobiCS InverseJacobiDC InverseJacobiDN InverseJacobiDS InverseJacobiNC InverseJacobiND InverseJacobiNS InverseJacobiSC InverseJacobiSD InverseJacobiSN InverseLaplaceTransform InversePermutation InverseRadon InverseSeries InverseSurvivalFunction InverseWaveletTransform InverseWeierstrassP InverseZTransform Invisible InvisibleApplication InvisibleTimes IrreduciblePolynomialQ IsolatingInterval IsomorphicGraphQ IsotopeData Italic Item ItemBox ItemBoxOptions ItemSize ItemStyle ItoProcess JaccardDissimilarity JacobiAmplitude Jacobian JacobiCD JacobiCN JacobiCS JacobiDC JacobiDN JacobiDS JacobiNC JacobiND JacobiNS JacobiP JacobiSC JacobiSD JacobiSN JacobiSymbol JacobiZeta JankoGroupJ1 JankoGroupJ2 JankoGroupJ3 JankoGroupJ4 JarqueBeraALMTest JohnsonDistribution Join Joined JoinedCurve JoinedCurveBox JoinForm JordanDecomposition JordanModelDecomposition K KagiChart KaiserBesselWindow KaiserWindow KalmanEstimator KalmanFilter KarhunenLoeveDecomposition KaryTree KatzCentrality KCoreComponents KDistribution KelvinBei KelvinBer KelvinKei KelvinKer KendallTau KendallTauTest KernelExecute KernelMixtureDistribution KernelObject Kernels Ket Khinchin KirchhoffGraph KirchhoffMatrix KleinInvariantJ KnightTourGraph KnotData KnownUnitQ KolmogorovSmirnovTest KroneckerDelta KroneckerModelDecomposition KroneckerProduct KroneckerSymbol KuiperTest KumaraswamyDistribution Kurtosis KuwaharaFilter Label Labeled LabeledSlider LabelingFunction LabelStyle LaguerreL LambdaComponents LambertW LanczosWindow LandauDistribution Language LanguageCategory LaplaceDistribution LaplaceTransform Laplacian LaplacianFilter LaplacianGaussianFilter Large Larger Last Latitude LatitudeLongitude LatticeData LatticeReduce Launch LaunchKernels LayeredGraphPlot LayerSizeFunction LayoutInformation LCM LeafCount LeapYearQ LeastSquares LeastSquaresFilterKernel Left LeftArrow LeftArrowBar LeftArrowRightArrow LeftDownTeeVector LeftDownVector LeftDownVectorBar LeftRightArrow LeftRightVector LeftTee LeftTeeArrow LeftTeeVector LeftTriangle LeftTriangleBar LeftTriangleEqual LeftUpDownVector LeftUpTeeVector LeftUpVector LeftUpVectorBar LeftVector LeftVectorBar LegendAppearance Legended LegendFunction LegendLabel LegendLayout LegendMargins LegendMarkers LegendMarkerSize LegendreP LegendreQ LegendreType Length LengthWhile LerchPhi Less LessEqual LessEqualGreater LessFullEqual LessGreater LessLess LessSlantEqual LessTilde LetterCharacter LetterQ Level LeveneTest LeviCivitaTensor LevyDistribution Lexicographic LibraryFunction LibraryFunctionError LibraryFunctionInformation LibraryFunctionLoad LibraryFunctionUnload LibraryLoad LibraryUnload LicenseID LiftingFilterData LiftingWaveletTransform LightBlue LightBrown LightCyan Lighter LightGray LightGreen Lighting LightingAngle LightMagenta LightOrange LightPink LightPurple LightRed LightSources LightYellow Likelihood Limit LimitsPositioning LimitsPositioningTokens LindleyDistribution Line Line3DBox LinearFilter LinearFractionalTransform LinearModelFit LinearOffsetFunction LinearProgramming LinearRecurrence LinearSolve LinearSolveFunction LineBox LineBreak LinebreakAdjustments LineBreakChart LineBreakWithin LineColor LineForm LineGraph LineIndent LineIndentMaxFraction LineIntegralConvolutionPlot LineIntegralConvolutionScale LineLegend LineOpacity LineSpacing LineWrapParts LinkActivate LinkClose LinkConnect LinkConnectedQ LinkCreate LinkError LinkFlush LinkFunction LinkHost LinkInterrupt LinkLaunch LinkMode LinkObject LinkOpen LinkOptions LinkPatterns LinkProtocol LinkRead LinkReadHeld LinkReadyQ Links LinkWrite LinkWriteHeld LiouvilleLambda List Listable ListAnimate ListContourPlot ListContourPlot3D ListConvolve ListCorrelate ListCurvePathPlot ListDeconvolve ListDensityPlot Listen ListFourierSequenceTransform ListInterpolation ListLineIntegralConvolutionPlot ListLinePlot ListLogLinearPlot ListLogLogPlot ListLogPlot ListPicker ListPickerBox ListPickerBoxBackground ListPickerBoxOptions ListPlay ListPlot ListPlot3D ListPointPlot3D ListPolarPlot ListQ ListStreamDensityPlot ListStreamPlot ListSurfacePlot3D ListVectorDensityPlot ListVectorPlot ListVectorPlot3D ListZTransform Literal LiteralSearch LocalClusteringCoefficient LocalizeVariables LocationEquivalenceTest LocationTest Locator LocatorAutoCreate LocatorBox LocatorBoxOptions LocatorCentering LocatorPane LocatorPaneBox LocatorPaneBoxOptions LocatorRegion Locked Log Log10 Log2 LogBarnesG LogGamma LogGammaDistribution LogicalExpand LogIntegral LogisticDistribution LogitModelFit LogLikelihood LogLinearPlot LogLogisticDistribution LogLogPlot LogMultinormalDistribution LogNormalDistribution LogPlot LogRankTest LogSeriesDistribution LongEqual Longest LongestAscendingSequence LongestCommonSequence LongestCommonSequencePositions LongestCommonSubsequence LongestCommonSubsequencePositions LongestMatch LongForm Longitude LongLeftArrow LongLeftRightArrow LongRightArrow Loopback LoopFreeGraphQ LowerCaseQ LowerLeftArrow LowerRightArrow LowerTriangularize LowpassFilter LQEstimatorGains LQGRegulator LQOutputRegulatorGains LQRegulatorGains LUBackSubstitution LucasL LuccioSamiComponents LUDecomposition LyapunovSolve LyonsGroupLy MachineID MachineName MachineNumberQ MachinePrecision MacintoshSystemPageSetup Magenta Magnification Magnify MainSolve MaintainDynamicCaches Majority MakeBoxes MakeExpression MakeRules MangoldtLambda ManhattanDistance Manipulate Manipulator MannWhitneyTest MantissaExponent Manual Map MapAll MapAt MapIndexed MAProcess MapThread MarcumQ MardiaCombinedTest MardiaKurtosisTest MardiaSkewnessTest MarginalDistribution MarkovProcessProperties Masking MatchingDissimilarity MatchLocalNameQ MatchLocalNames MatchQ Material MathematicaNotation MathieuC MathieuCharacteristicA MathieuCharacteristicB MathieuCharacteristicExponent MathieuCPrime MathieuGroupM11 MathieuGroupM12 MathieuGroupM22 MathieuGroupM23 MathieuGroupM24 MathieuS MathieuSPrime MathMLForm MathMLText Matrices MatrixExp MatrixForm MatrixFunction MatrixLog MatrixPlot MatrixPower MatrixQ MatrixRank Max MaxBend MaxDetect MaxExtraBandwidths MaxExtraConditions MaxFeatures MaxFilter Maximize MaxIterations MaxMemoryUsed MaxMixtureKernels MaxPlotPoints MaxPoints MaxRecursion MaxStableDistribution MaxStepFraction MaxSteps MaxStepSize MaxValue MaxwellDistribution McLaughlinGroupMcL Mean MeanClusteringCoefficient MeanDegreeConnectivity MeanDeviation MeanFilter MeanGraphDistance MeanNeighborDegree MeanShift MeanShiftFilter Median MedianDeviation MedianFilter Medium MeijerG MeixnerDistribution MemberQ MemoryConstrained MemoryInUse Menu MenuAppearance MenuCommandKey MenuEvaluator MenuItem MenuPacket MenuSortingValue MenuStyle MenuView MergeDifferences Mesh MeshFunctions MeshRange MeshShading MeshStyle Message MessageDialog MessageList MessageName MessageOptions MessagePacket Messages MessagesNotebook MetaCharacters MetaInformation Method MethodOptions MexicanHatWavelet MeyerWavelet Min MinDetect MinFilter MinimalPolynomial MinimalStateSpaceModel Minimize Minors MinRecursion MinSize MinStableDistribution Minus MinusPlus MinValue Missing MissingDataMethod MittagLefflerE MixedRadix MixedRadixQuantity MixtureDistribution Mod Modal Mode Modular ModularLambda Module Modulus MoebiusMu Moment Momentary MomentConvert MomentEvaluate MomentGeneratingFunction Monday Monitor MonomialList MonomialOrder MonsterGroupM MorletWavelet MorphologicalBinarize MorphologicalBranchPoints MorphologicalComponents MorphologicalEulerNumber MorphologicalGraph MorphologicalPerimeter MorphologicalTransform Most MouseAnnotation MouseAppearance MouseAppearanceTag MouseButtons Mouseover MousePointerNote MousePosition MovingAverage MovingMedian MoyalDistribution MultiedgeStyle MultilaunchWarning MultiLetterItalics MultiLetterStyle MultilineFunction Multinomial MultinomialDistribution MultinormalDistribution MultiplicativeOrder Multiplicity Multiselection MultivariateHypergeometricDistribution MultivariatePoissonDistribution MultivariateTDistribution N NakagamiDistribution NameQ Names NamespaceBox Nand NArgMax NArgMin NBernoulliB NCache NDSolve NDSolveValue Nearest NearestFunction NeedCurrentFrontEndPackagePacket NeedCurrentFrontEndSymbolsPacket NeedlemanWunschSimilarity Needs Negative NegativeBinomialDistribution NegativeMultinomialDistribution NeighborhoodGraph Nest NestedGreaterGreater NestedLessLess NestedScriptRules NestList NestWhile NestWhileList NevilleThetaC NevilleThetaD NevilleThetaN NevilleThetaS NewPrimitiveStyle NExpectation Next NextPrime NHoldAll NHoldFirst NHoldRest NicholsGridLines NicholsPlot NIntegrate NMaximize NMaxValue NMinimize NMinValue NominalVariables NonAssociative NoncentralBetaDistribution NoncentralChiSquareDistribution NoncentralFRatioDistribution NoncentralStudentTDistribution NonCommutativeMultiply NonConstants None NonlinearModelFit NonlocalMeansFilter NonNegative NonPositive Nor NorlundB Norm Normal NormalDistribution NormalGrouping Normalize NormalizedSquaredEuclideanDistance NormalsFunction NormFunction Not NotCongruent NotCupCap NotDoubleVerticalBar Notebook NotebookApply NotebookAutoSave NotebookClose NotebookConvertSettings NotebookCreate NotebookCreateReturnObject NotebookDefault NotebookDelete NotebookDirectory NotebookDynamicExpression NotebookEvaluate NotebookEventActions NotebookFileName NotebookFind NotebookFindReturnObject NotebookGet NotebookGetLayoutInformationPacket NotebookGetMisspellingsPacket NotebookInformation NotebookInterfaceObject NotebookLocate NotebookObject NotebookOpen NotebookOpenReturnObject NotebookPath NotebookPrint NotebookPut NotebookPutReturnObject NotebookRead NotebookResetGeneratedCells Notebooks NotebookSave NotebookSaveAs NotebookSelection NotebookSetupLayoutInformationPacket NotebooksMenu NotebookWrite NotElement NotEqualTilde NotExists NotGreater NotGreaterEqual NotGreaterFullEqual NotGreaterGreater NotGreaterLess NotGreaterSlantEqual NotGreaterTilde NotHumpDownHump NotHumpEqual NotLeftTriangle NotLeftTriangleBar NotLeftTriangleEqual NotLess NotLessEqual NotLessFullEqual NotLessGreater NotLessLess NotLessSlantEqual NotLessTilde NotNestedGreaterGreater NotNestedLessLess NotPrecedes NotPrecedesEqual NotPrecedesSlantEqual NotPrecedesTilde NotReverseElement NotRightTriangle NotRightTriangleBar NotRightTriangleEqual NotSquareSubset NotSquareSubsetEqual NotSquareSuperset NotSquareSupersetEqual NotSubset NotSubsetEqual NotSucceeds NotSucceedsEqual NotSucceedsSlantEqual NotSucceedsTilde NotSuperset NotSupersetEqual NotTilde NotTildeEqual NotTildeFullEqual NotTildeTilde NotVerticalBar NProbability NProduct NProductFactors NRoots NSolve NSum NSumTerms Null NullRecords NullSpace NullWords Number NumberFieldClassNumber NumberFieldDiscriminant NumberFieldFundamentalUnits NumberFieldIntegralBasis NumberFieldNormRepresentatives NumberFieldRegulator NumberFieldRootsOfUnity NumberFieldSignature NumberForm NumberFormat NumberMarks NumberMultiplier NumberPadding NumberPoint NumberQ NumberSeparator NumberSigns NumberString Numerator NumericFunction NumericQ NuttallWindow NValues NyquistGridLines NyquistPlot O ObservabilityGramian ObservabilityMatrix ObservableDecomposition ObservableModelQ OddQ Off Offset OLEData On ONanGroupON OneIdentity Opacity Open OpenAppend Opener OpenerBox OpenerBoxOptions OpenerView OpenFunctionInspectorPacket Opening OpenRead OpenSpecialOptions OpenTemporary OpenWrite Operate OperatingSystem OptimumFlowData Optional OptionInspectorSettings OptionQ Options OptionsPacket OptionsPattern OptionValue OptionValueBox OptionValueBoxOptions Or Orange Order OrderDistribution OrderedQ Ordering Orderless OrnsteinUhlenbeckProcess Orthogonalize Out Outer OutputAutoOverwrite OutputControllabilityMatrix OutputControllableModelQ OutputForm OutputFormData OutputGrouping OutputMathEditExpression OutputNamePacket OutputResponse OutputSizeLimit OutputStream Over OverBar OverDot Overflow OverHat Overlaps Overlay OverlayBox OverlayBoxOptions Overscript OverscriptBox OverscriptBoxOptions OverTilde OverVector OwenT OwnValues PackingMethod PaddedForm Padding PadeApproximant PadLeft PadRight PageBreakAbove PageBreakBelow PageBreakWithin PageFooterLines PageFooters PageHeaderLines PageHeaders PageHeight PageRankCentrality PageWidth PairedBarChart PairedHistogram PairedSmoothHistogram PairedTTest PairedZTest PaletteNotebook PalettePath Pane PaneBox PaneBoxOptions Panel PanelBox PanelBoxOptions Paneled PaneSelector PaneSelectorBox PaneSelectorBoxOptions PaperWidth ParabolicCylinderD ParagraphIndent ParagraphSpacing ParallelArray ParallelCombine ParallelDo ParallelEvaluate Parallelization Parallelize ParallelMap ParallelNeeds ParallelProduct ParallelSubmit ParallelSum ParallelTable ParallelTry Parameter ParameterEstimator ParameterMixtureDistribution ParameterVariables ParametricFunction ParametricNDSolve ParametricNDSolveValue ParametricPlot ParametricPlot3D ParentConnect ParentDirectory ParentForm Parenthesize ParentList ParetoDistribution Part PartialCorrelationFunction PartialD ParticleData Partition PartitionsP PartitionsQ ParzenWindow PascalDistribution PassEventsDown PassEventsUp Paste PasteBoxFormInlineCells PasteButton Path PathGraph PathGraphQ Pattern PatternSequence PatternTest PauliMatrix PaulWavelet Pause PausedTime PDF PearsonChiSquareTest PearsonCorrelationTest PearsonDistribution PerformanceGoal PeriodicInterpolation Periodogram PeriodogramArray PermutationCycles PermutationCyclesQ PermutationGroup PermutationLength PermutationList PermutationListQ PermutationMax PermutationMin PermutationOrder PermutationPower PermutationProduct PermutationReplace Permutations PermutationSupport Permute PeronaMalikFilter Perpendicular PERTDistribution PetersenGraph PhaseMargins Pi Pick PIDData PIDDerivativeFilter PIDFeedforward PIDTune Piecewise PiecewiseExpand PieChart PieChart3D PillaiTrace PillaiTraceTest Pink Pivoting PixelConstrained PixelValue PixelValuePositions Placed Placeholder PlaceholderReplace Plain PlanarGraphQ Play PlayRange Plot Plot3D Plot3Matrix PlotDivision PlotJoined PlotLabel PlotLayout PlotLegends PlotMarkers PlotPoints PlotRange PlotRangeClipping PlotRangePadding PlotRegion PlotStyle Plus PlusMinus Pochhammer PodStates PodWidth Point Point3DBox PointBox PointFigureChart PointForm PointLegend PointSize PoissonConsulDistribution PoissonDistribution PoissonProcess PoissonWindow PolarAxes PolarAxesOrigin PolarGridLines PolarPlot PolarTicks PoleZeroMarkers PolyaAeppliDistribution PolyGamma Polygon Polygon3DBox Polygon3DBoxOptions PolygonBox PolygonBoxOptions PolygonHoleScale PolygonIntersections PolygonScale PolyhedronData PolyLog PolynomialExtendedGCD PolynomialForm PolynomialGCD PolynomialLCM PolynomialMod PolynomialQ PolynomialQuotient PolynomialQuotientRemainder PolynomialReduce PolynomialRemainder Polynomials PopupMenu PopupMenuBox PopupMenuBoxOptions PopupView PopupWindow Position Positive PositiveDefiniteMatrixQ PossibleZeroQ Postfix PostScript Power PowerDistribution PowerExpand PowerMod PowerModList PowerSpectralDensity PowersRepresentations PowerSymmetricPolynomial Precedence PrecedenceForm Precedes PrecedesEqual PrecedesSlantEqual PrecedesTilde Precision PrecisionGoal PreDecrement PredictionRoot PreemptProtect PreferencesPath Prefix PreIncrement Prepend PrependTo PreserveImageOptions Previous PriceGraphDistribution PrimaryPlaceholder Prime PrimeNu PrimeOmega PrimePi PrimePowerQ PrimeQ Primes PrimeZetaP PrimitiveRoot PrincipalComponents PrincipalValue Print PrintAction PrintForm PrintingCopies PrintingOptions PrintingPageRange PrintingStartingPageNumber PrintingStyleEnvironment PrintPrecision PrintTemporary Prism PrismBox PrismBoxOptions PrivateCellOptions PrivateEvaluationOptions PrivateFontOptions PrivateFrontEndOptions PrivateNotebookOptions PrivatePaths Probability ProbabilityDistribution ProbabilityPlot ProbabilityPr ProbabilityScalePlot ProbitModelFit ProcessEstimator ProcessParameterAssumptions ProcessParameterQ ProcessStateDomain ProcessTimeDomain Product ProductDistribution ProductLog ProgressIndicator ProgressIndicatorBox ProgressIndicatorBoxOptions Projection Prolog PromptForm Properties Property PropertyList PropertyValue Proportion Proportional Protect Protected ProteinData Pruning PseudoInverse Purple Put PutAppend Pyramid PyramidBox PyramidBoxOptions QBinomial QFactorial QGamma QHypergeometricPFQ QPochhammer QPolyGamma QRDecomposition QuadraticIrrationalQ Quantile QuantilePlot Quantity QuantityForm QuantityMagnitude QuantityQ QuantityUnit Quartics QuartileDeviation Quartiles QuartileSkewness QueueingNetworkProcess QueueingProcess QueueProperties Quiet Quit Quotient QuotientRemainder RadialityCentrality RadicalBox RadicalBoxOptions RadioButton RadioButtonBar RadioButtonBox RadioButtonBoxOptions Radon RamanujanTau RamanujanTauL RamanujanTauTheta RamanujanTauZ Random RandomChoice RandomComplex RandomFunction RandomGraph RandomImage RandomInteger RandomPermutation RandomPrime RandomReal RandomSample RandomSeed RandomVariate RandomWalkProcess Range RangeFilter RangeSpecification RankedMax RankedMin Raster Raster3D Raster3DBox Raster3DBoxOptions RasterArray RasterBox RasterBoxOptions Rasterize RasterSize Rational RationalFunctions Rationalize Rationals Ratios Raw RawArray RawBoxes RawData RawMedium RayleighDistribution Re Read ReadList ReadProtected Real RealBlockDiagonalForm RealDigits RealExponent Reals Reap Record RecordLists RecordSeparators Rectangle RectangleBox RectangleBoxOptions RectangleChart RectangleChart3D RecurrenceFilter RecurrenceTable RecurringDigitsForm Red Reduce RefBox ReferenceLineStyle ReferenceMarkers ReferenceMarkerStyle Refine ReflectionMatrix ReflectionTransform Refresh RefreshRate RegionBinarize RegionFunction RegionPlot RegionPlot3D RegularExpression Regularization Reinstall Release ReleaseHold ReliabilityDistribution ReliefImage ReliefPlot Remove RemoveAlphaChannel RemoveAsynchronousTask Removed RemoveInputStreamMethod RemoveOutputStreamMethod RemoveProperty RemoveScheduledTask RenameDirectory RenameFile RenderAll RenderingOptions RenewalProcess RenkoChart Repeated RepeatedNull RepeatedString Replace ReplaceAll ReplaceHeldPart ReplaceImageValue ReplaceList ReplacePart ReplacePixelValue ReplaceRepeated Resampling Rescale RescalingTransform ResetDirectory ResetMenusPacket ResetScheduledTask Residue Resolve Rest Resultant ResumePacket Return ReturnExpressionPacket ReturnInputFormPacket ReturnPacket ReturnTextPacket Reverse ReverseBiorthogonalSplineWavelet ReverseElement ReverseEquilibrium ReverseGraph ReverseUpEquilibrium RevolutionAxis RevolutionPlot3D RGBColor RiccatiSolve RiceDistribution RidgeFilter RiemannR RiemannSiegelTheta RiemannSiegelZ Riffle Right RightArrow RightArrowBar RightArrowLeftArrow RightCosetRepresentative RightDownTeeVector RightDownVector RightDownVectorBar RightTee RightTeeArrow RightTeeVector RightTriangle RightTriangleBar RightTriangleEqual RightUpDownVector RightUpTeeVector RightUpVector RightUpVectorBar RightVector RightVectorBar RiskAchievementImportance RiskReductionImportance RogersTanimotoDissimilarity Root RootApproximant RootIntervals RootLocusPlot RootMeanSquare RootOfUnityQ RootReduce Roots RootSum Rotate RotateLabel RotateLeft RotateRight RotationAction RotationBox RotationBoxOptions RotationMatrix RotationTransform Round RoundImplies RoundingRadius Row RowAlignments RowBackgrounds RowBox RowHeights RowLines RowMinHeight RowReduce RowsEqual RowSpacings RSolve RudvalisGroupRu Rule RuleCondition RuleDelayed RuleForm RulerUnits Run RunScheduledTask RunThrough RuntimeAttributes RuntimeOptions RussellRaoDissimilarity SameQ SameTest SampleDepth SampledSoundFunction SampledSoundList SampleRate SamplingPeriod SARIMAProcess SARMAProcess SatisfiabilityCount SatisfiabilityInstances SatisfiableQ Saturday Save Saveable SaveAutoDelete SaveDefinitions SawtoothWave Scale Scaled ScaleDivisions ScaledMousePosition ScaleOrigin ScalePadding ScaleRanges ScaleRangeStyle ScalingFunctions ScalingMatrix ScalingTransform Scan ScheduledTaskActiveQ ScheduledTaskData ScheduledTaskObject ScheduledTasks SchurDecomposition ScientificForm ScreenRectangle ScreenStyleEnvironment ScriptBaselineShifts ScriptLevel ScriptMinSize ScriptRules ScriptSizeMultipliers Scrollbars ScrollingOptions ScrollPosition Sec Sech SechDistribution SectionGrouping SectorChart SectorChart3D SectorOrigin SectorSpacing SeedRandom Select Selectable SelectComponents SelectedCells SelectedNotebook Selection SelectionAnimate SelectionCell SelectionCellCreateCell SelectionCellDefaultStyle SelectionCellParentStyle SelectionCreateCell SelectionDebuggerTag SelectionDuplicateCell SelectionEvaluate SelectionEvaluateCreateCell SelectionMove SelectionPlaceholder SelectionSetStyle SelectWithContents SelfLoops SelfLoopStyle SemialgebraicComponentInstances SendMail Sequence SequenceAlignment SequenceForm SequenceHold SequenceLimit Series SeriesCoefficient SeriesData SessionTime Set SetAccuracy SetAlphaChannel SetAttributes Setbacks SetBoxFormNamesPacket SetDelayed SetDirectory SetEnvironment SetEvaluationNotebook SetFileDate SetFileLoadingContext SetNotebookStatusLine SetOptions SetOptionsPacket SetPrecision SetProperty SetSelectedNotebook SetSharedFunction SetSharedVariable SetSpeechParametersPacket SetStreamPosition SetSystemOptions Setter SetterBar SetterBox SetterBoxOptions Setting SetValue Shading Shallow ShannonWavelet ShapiroWilkTest Share Sharpen ShearingMatrix ShearingTransform ShenCastanMatrix Short ShortDownArrow Shortest ShortestMatch ShortestPathFunction ShortLeftArrow ShortRightArrow ShortUpArrow Show ShowAutoStyles ShowCellBracket ShowCellLabel ShowCellTags ShowClosedCellArea ShowContents ShowControls ShowCursorTracker ShowGroupOpenCloseIcon ShowGroupOpener ShowInvisibleCharacters ShowPageBreaks ShowPredictiveInterface ShowSelection ShowShortBoxForm ShowSpecialCharacters ShowStringCharacters ShowSyntaxStyles ShrinkingDelay ShrinkWrapBoundingBox SiegelTheta SiegelTukeyTest Sign Signature SignedRankTest SignificanceLevel SignPadding SignTest SimilarityRules SimpleGraph SimpleGraphQ Simplify Sin Sinc SinghMaddalaDistribution SingleEvaluation SingleLetterItalics SingleLetterStyle SingularValueDecomposition SingularValueList SingularValuePlot SingularValues Sinh SinhIntegral SinIntegral SixJSymbol Skeleton SkeletonTransform SkellamDistribution Skewness SkewNormalDistribution Skip SliceDistribution Slider Slider2D Slider2DBox Slider2DBoxOptions SliderBox SliderBoxOptions SlideView Slot SlotSequence Small SmallCircle Smaller SmithDelayCompensator SmithWatermanSimilarity SmoothDensityHistogram SmoothHistogram SmoothHistogram3D SmoothKernelDistribution SocialMediaData Socket SokalSneathDissimilarity Solve SolveAlways SolveDelayed Sort SortBy Sound SoundAndGraphics SoundNote SoundVolume Sow Space SpaceForm Spacer Spacings Span SpanAdjustments SpanCharacterRounding SpanFromAbove SpanFromBoth SpanFromLeft SpanLineThickness SpanMaxSize SpanMinSize SpanningCharacters SpanSymmetric SparseArray SpatialGraphDistribution Speak SpeakTextPacket SpearmanRankTest SpearmanRho Spectrogram SpectrogramArray Specularity SpellingCorrection SpellingDictionaries SpellingDictionariesPath SpellingOptions SpellingSuggestionsPacket Sphere SphereBox SphericalBesselJ SphericalBesselY SphericalHankelH1 SphericalHankelH2 SphericalHarmonicY SphericalPlot3D SphericalRegion SpheroidalEigenvalue SpheroidalJoiningFactor SpheroidalPS SpheroidalPSPrime SpheroidalQS SpheroidalQSPrime SpheroidalRadialFactor SpheroidalS1 SpheroidalS1Prime SpheroidalS2 SpheroidalS2Prime Splice SplicedDistribution SplineClosed SplineDegree SplineKnots SplineWeights Split SplitBy SpokenString Sqrt SqrtBox SqrtBoxOptions Square SquaredEuclideanDistance SquareFreeQ SquareIntersection SquaresR SquareSubset SquareSubsetEqual SquareSuperset SquareSupersetEqual SquareUnion SquareWave StabilityMargins StabilityMarginsStyle StableDistribution Stack StackBegin StackComplete StackInhibit StandardDeviation StandardDeviationFilter StandardForm Standardize StandbyDistribution Star StarGraph StartAsynchronousTask StartingStepSize StartOfLine StartOfString StartScheduledTask StartupSound StateDimensions StateFeedbackGains StateOutputEstimator StateResponse StateSpaceModel StateSpaceRealization StateSpaceTransform StationaryDistribution StationaryWaveletPacketTransform StationaryWaveletTransform StatusArea StatusCentrality StepMonitor StieltjesGamma StirlingS1 StirlingS2 StopAsynchronousTask StopScheduledTask StrataVariables StratonovichProcess StreamColorFunction StreamColorFunctionScaling StreamDensityPlot StreamPlot StreamPoints StreamPosition Streams StreamScale StreamStyle String StringBreak StringByteCount StringCases StringCount StringDrop StringExpression StringForm StringFormat StringFreeQ StringInsert StringJoin StringLength StringMatchQ StringPosition StringQ StringReplace StringReplaceList StringReplacePart StringReverse StringRotateLeft StringRotateRight StringSkeleton StringSplit StringTake StringToStream StringTrim StripBoxes StripOnInput StripWrapperBoxes StrokeForm StructuralImportance StructuredArray StructuredSelection StruveH StruveL Stub StudentTDistribution Style StyleBox StyleBoxAutoDelete StyleBoxOptions StyleData StyleDefinitions StyleForm StyleKeyMapping StyleMenuListing StyleNameDialogSettings StyleNames StylePrint StyleSheetPath Subfactorial Subgraph SubMinus SubPlus SubresultantPolynomialRemainders SubresultantPolynomials Subresultants Subscript SubscriptBox SubscriptBoxOptions Subscripted Subset SubsetEqual Subsets SubStar Subsuperscript SubsuperscriptBox SubsuperscriptBoxOptions Subtract SubtractFrom SubValues Succeeds SucceedsEqual SucceedsSlantEqual SucceedsTilde SuchThat Sum SumConvergence Sunday SuperDagger SuperMinus SuperPlus Superscript SuperscriptBox SuperscriptBoxOptions Superset SupersetEqual SuperStar Surd SurdForm SurfaceColor SurfaceGraphics SurvivalDistribution SurvivalFunction SurvivalModel SurvivalModelFit SuspendPacket SuzukiDistribution SuzukiGroupSuz SwatchLegend Switch Symbol SymbolName SymletWavelet Symmetric SymmetricGroup SymmetricMatrixQ SymmetricPolynomial SymmetricReduction Symmetrize SymmetrizedArray SymmetrizedArrayRules SymmetrizedDependentComponents SymmetrizedIndependentComponents SymmetrizedReplacePart SynchronousInitialization SynchronousUpdating Syntax SyntaxForm SyntaxInformation SyntaxLength SyntaxPacket SyntaxQ SystemDialogInput SystemException SystemHelpPath SystemInformation SystemInformationData SystemOpen SystemOptions SystemsModelDelay SystemsModelDelayApproximate SystemsModelDelete SystemsModelDimensions SystemsModelExtract SystemsModelFeedbackConnect SystemsModelLabels SystemsModelOrder SystemsModelParallelConnect SystemsModelSeriesConnect SystemsModelStateFeedbackConnect SystemStub Tab TabFilling Table TableAlignments TableDepth TableDirections TableForm TableHeadings TableSpacing TableView TableViewBox TabSpacings TabView TabViewBox TabViewBoxOptions TagBox TagBoxNote TagBoxOptions TaggingRules TagSet TagSetDelayed TagStyle TagUnset Take TakeWhile Tally Tan Tanh TargetFunctions TargetUnits TautologyQ TelegraphProcess TemplateBox TemplateBoxOptions TemplateSlotSequence TemporalData Temporary TemporaryVariable TensorContract TensorDimensions TensorExpand TensorProduct TensorQ TensorRank TensorReduce TensorSymmetry TensorTranspose TensorWedge Tetrahedron TetrahedronBox TetrahedronBoxOptions TeXForm TeXSave Text Text3DBox Text3DBoxOptions TextAlignment TextBand TextBoundingBox TextBox TextCell TextClipboardType TextData TextForm TextJustification TextLine TextPacket TextParagraph TextRecognize TextRendering TextStyle Texture TextureCoordinateFunction TextureCoordinateScaling Therefore ThermometerGauge Thick Thickness Thin Thinning ThisLink ThompsonGroupTh Thread ThreeJSymbol Threshold Through Throw Thumbnail Thursday Ticks TicksStyle Tilde TildeEqual TildeFullEqual TildeTilde TimeConstrained TimeConstraint Times TimesBy TimeSeriesForecast TimeSeriesInvertibility TimeUsed TimeValue TimeZone Timing Tiny TitleGrouping TitsGroupT ToBoxes ToCharacterCode ToColor ToContinuousTimeModel ToDate ToDiscreteTimeModel ToeplitzMatrix ToExpression ToFileName Together Toggle ToggleFalse Toggler TogglerBar TogglerBox TogglerBoxOptions ToHeldExpression ToInvertibleTimeSeries TokenWords Tolerance ToLowerCase ToNumberField TooBig Tooltip TooltipBox TooltipBoxOptions TooltipDelay TooltipStyle Top TopHatTransform TopologicalSort ToRadicals ToRules ToString Total TotalHeight TotalVariationFilter TotalWidth TouchscreenAutoZoom TouchscreenControlPlacement ToUpperCase Tr Trace TraceAbove TraceAction TraceBackward TraceDepth TraceDialog TraceForward TraceInternal TraceLevel TraceOff TraceOn TraceOriginal TracePrint TraceScan TrackedSymbols TradingChart TraditionalForm TraditionalFunctionNotation TraditionalNotation TraditionalOrder TransferFunctionCancel TransferFunctionExpand TransferFunctionFactor TransferFunctionModel TransferFunctionPoles TransferFunctionTransform TransferFunctionZeros TransformationFunction TransformationFunctions TransformationMatrix TransformedDistribution TransformedField Translate TranslationTransform TransparentColor Transpose TreeForm TreeGraph TreeGraphQ TreePlot TrendStyle TriangleWave TriangularDistribution Trig TrigExpand TrigFactor TrigFactorList Trigger TrigReduce TrigToExp TrimmedMean True TrueQ TruncatedDistribution TsallisQExponentialDistribution TsallisQGaussianDistribution TTest Tube TubeBezierCurveBox TubeBezierCurveBoxOptions TubeBox TubeBSplineCurveBox TubeBSplineCurveBoxOptions Tuesday TukeyLambdaDistribution TukeyWindow Tuples TuranGraph TuringMachine Transparent UnateQ Uncompress Undefined UnderBar Underflow Underlined Underoverscript UnderoverscriptBox UnderoverscriptBoxOptions Underscript UnderscriptBox UnderscriptBoxOptions UndirectedEdge UndirectedGraph UndirectedGraphQ UndocumentedTestFEParserPacket UndocumentedTestGetSelectionPacket Unequal Unevaluated UniformDistribution UniformGraphDistribution UniformSumDistribution Uninstall Union UnionPlus Unique UnitBox UnitConvert UnitDimensions Unitize UnitRootTest UnitSimplify UnitStep UnitTriangle UnitVector Unprotect UnsameQ UnsavedVariables Unset UnsetShared UntrackedVariables Up UpArrow UpArrowBar UpArrowDownArrow Update UpdateDynamicObjects UpdateDynamicObjectsSynchronous UpdateInterval UpDownArrow UpEquilibrium UpperCaseQ UpperLeftArrow UpperRightArrow UpperTriangularize Upsample UpSet UpSetDelayed UpTee UpTeeArrow UpValues URL URLFetch URLFetchAsynchronous URLSave URLSaveAsynchronous UseGraphicsRange Using UsingFrontEnd V2Get ValidationLength Value ValueBox ValueBoxOptions ValueForm ValueQ ValuesData Variables Variance VarianceEquivalenceTest VarianceEstimatorFunction VarianceGammaDistribution VarianceTest VectorAngle VectorColorFunction VectorColorFunctionScaling VectorDensityPlot VectorGlyphData VectorPlot VectorPlot3D VectorPoints VectorQ Vectors VectorScale VectorStyle Vee Verbatim Verbose VerboseConvertToPostScriptPacket VerifyConvergence VerifySolutions VerifyTestAssumptions Version VersionNumber VertexAdd VertexCapacity VertexColors VertexComponent VertexConnectivity VertexCoordinateRules VertexCoordinates VertexCorrelationSimilarity VertexCosineSimilarity VertexCount VertexCoverQ VertexDataCoordinates VertexDegree VertexDelete VertexDiceSimilarity VertexEccentricity VertexInComponent VertexInDegree VertexIndex VertexJaccardSimilarity VertexLabeling VertexLabels VertexLabelStyle VertexList VertexNormals VertexOutComponent VertexOutDegree VertexQ VertexRenderingFunction VertexReplace VertexShape VertexShapeFunction VertexSize VertexStyle VertexTextureCoordinates VertexWeight Vertical VerticalBar VerticalForm VerticalGauge VerticalSeparator VerticalSlider VerticalTilde ViewAngle ViewCenter ViewMatrix ViewPoint ViewPointSelectorSettings ViewPort ViewRange ViewVector ViewVertical VirtualGroupData Visible VisibleCell VoigtDistribution VonMisesDistribution WaitAll WaitAsynchronousTask WaitNext WaitUntil WakebyDistribution WalleniusHypergeometricDistribution WaringYuleDistribution WatershedComponents WatsonUSquareTest WattsStrogatzGraphDistribution WaveletBestBasis WaveletFilterCoefficients WaveletImagePlot WaveletListPlot WaveletMapIndexed WaveletMatrixPlot WaveletPhi WaveletPsi WaveletScale WaveletScalogram WaveletThreshold WeaklyConnectedComponents WeaklyConnectedGraphQ WeakStationarity WeatherData WeberE Wedge Wednesday WeibullDistribution WeierstrassHalfPeriods WeierstrassInvariants WeierstrassP WeierstrassPPrime WeierstrassSigma WeierstrassZeta WeightedAdjacencyGraph WeightedAdjacencyMatrix WeightedData WeightedGraphQ Weights WelchWindow WheelGraph WhenEvent Which While White Whitespace WhitespaceCharacter WhittakerM WhittakerW WienerFilter WienerProcess WignerD WignerSemicircleDistribution WilksW WilksWTest WindowClickSelect WindowElements WindowFloating WindowFrame WindowFrameElements WindowMargins WindowMovable WindowOpacity WindowSelected WindowSize WindowStatusArea WindowTitle WindowToolbars WindowWidth With WolframAlpha WolframAlphaDate WolframAlphaQuantity WolframAlphaResult Word WordBoundary WordCharacter WordData WordSearch WordSeparators WorkingPrecision Write WriteString Wronskian XMLElement XMLObject Xnor Xor Yellow YuleDissimilarity ZernikeR ZeroSymmetric ZeroTest ZeroWidthTimes Zeta ZetaZero ZipfDistribution ZTest ZTransform $Aborted $ActivationGroupID $ActivationKey $ActivationUserRegistered $AddOnsDirectory $AssertFunction $Assumptions $AsynchronousTask $BaseDirectory $BatchInput $BatchOutput $BoxForms $ByteOrdering $Canceled $CharacterEncoding $CharacterEncodings $CommandLine $CompilationTarget $ConditionHold $ConfiguredKernels $Context $ContextPath $ControlActiveSetting $CreationDate $CurrentLink $DateStringFormat $DefaultFont $DefaultFrontEnd $DefaultImagingDevice $DefaultPath $Display $DisplayFunction $DistributedContexts $DynamicEvaluation $Echo $Epilog $ExportFormats $Failed $FinancialDataSource $FormatType $FrontEnd $FrontEndSession $GeoLocation $HistoryLength $HomeDirectory $HTTPCookies $IgnoreEOF $ImagingDevices $ImportFormats $InitialDirectory $Input $InputFileName $InputStreamMethods $Inspector $InstallationDate $InstallationDirectory $InterfaceEnvironment $IterationLimit $KernelCount $KernelID $Language $LaunchDirectory $LibraryPath $LicenseExpirationDate $LicenseID $LicenseProcesses $LicenseServer $LicenseSubprocesses $LicenseType $Line $Linked $LinkSupported $LoadedFiles $MachineAddresses $MachineDomain $MachineDomains $MachineEpsilon $MachineID $MachineName $MachinePrecision $MachineType $MaxExtraPrecision $MaxLicenseProcesses $MaxLicenseSubprocesses $MaxMachineNumber $MaxNumber $MaxPiecewiseCases $MaxPrecision $MaxRootDegree $MessageGroups $MessageList $MessagePrePrint $Messages $MinMachineNumber $MinNumber $MinorReleaseNumber $MinPrecision $ModuleNumber $NetworkLicense $NewMessage $NewSymbol $Notebooks $NumberMarks $Off $OperatingSystem $Output $OutputForms $OutputSizeLimit $OutputStreamMethods $Packages $ParentLink $ParentProcessID $PasswordFile $PatchLevelID $Path $PathnameSeparator $PerformanceGoal $PipeSupported $Post $Pre $PreferencesDirectory $PrePrint $PreRead $PrintForms $PrintLiteral $ProcessID $ProcessorCount $ProcessorType $ProductInformation $ProgramName $RandomState $RecursionLimit $ReleaseNumber $RootDirectory $ScheduledTask $ScriptCommandLine $SessionID $SetParentLink $SharedFunctions $SharedVariables $SoundDisplay $SoundDisplayFunction $SuppressInputFormHeads $SynchronousEvaluation $SyntaxHandler $System $SystemCharacterEncoding $SystemID $SystemWordLength $TemporaryDirectory $TemporaryPrefix $TextStyle $TimedOut $TimeUnit $TimeZone $TopDirectory $TraceOff $TraceOn $TracePattern $TracePostAction $TracePreAction $Urgent $UserAddOnsDirectory $UserBaseDirectory $UserDocumentsDirectory $UserName $Version $VersionNumber",
-contains:[{className:"comment",begin:/\(\*/,end:/\*\)/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{begin:/\{/,end:/\}/,illegal:/:/}]}});b.registerLanguage("matlab",function(a){var b=[a.C_NUMBER_MODE,{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]}],d={relevance:0,contains:[{begin:/'['\.]*/}]};return{keywords:{keyword:"break case catch classdef continue else elseif end enumerated events for function global if methods otherwise parfor persistent properties return spmd switch try while",
-built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun legend intersect ismember procrustes hold num2cell "},
-illegal:'(//|"|#|/\\*|\\s+/\\w+)',contains:[{className:"function",beginKeywords:"function",end:"$",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}]}]},{begin:/[a-zA-Z_][a-zA-Z_0-9]*'['\.]*/,returnBegin:!0,relevance:0,contains:[{begin:/[a-zA-Z_][a-zA-Z_0-9]*/,relevance:0},d.contains[0]]},{begin:"\\[",end:"\\]",contains:b,relevance:0,starts:d},{begin:"\\{",end:/}/,contains:b,relevance:0,starts:d},{begin:/\)/,relevance:0,starts:d},a.COMMENT("^\\s*\\%\\{\\s*$",
-"^\\s*\\%\\}\\s*$"),a.COMMENT("\\%","$")].concat(b)}});b.registerLanguage("maxima",function(a){return{lexemes:"[A-Za-z_%][0-9A-Za-z_%]*",keywords:{keyword:"if then else elseif for thru do while unless step in and or not",literal:"true false unknown inf minf ind und %e %i %pi %phi %gamma",built_in:" abasep abs absint absolute_real_time acos acosh acot acoth acsc acsch activate addcol add_edge add_edges addmatrices addrow add_vertex add_vertices adjacency_matrix adjoin adjoint af agd airy airy_ai airy_bi airy_dai airy_dbi algsys alg_type alias allroots alphacharp alphanumericp amortization %and annuity_fv annuity_pv antid antidiff AntiDifference append appendfile apply apply1 apply2 applyb1 apropos args arit_amortization arithmetic arithsum array arrayapply arrayinfo arraymake arraysetapply ascii asec asech asin asinh askinteger asksign assoc assoc_legendre_p assoc_legendre_q assume assume_external_byte_order asympa at atan atan2 atanh atensimp atom atvalue augcoefmatrix augmented_lagrangian_method av average_degree backtrace bars barsplot barsplot_description base64 base64_decode bashindices batch batchload bc2 bdvac belln benefit_cost bern bernpoly bernstein_approx bernstein_expand bernstein_poly bessel bessel_i bessel_j bessel_k bessel_simplify bessel_y beta beta_incomplete beta_incomplete_generalized beta_incomplete_regularized bezout bfallroots bffac bf_find_root bf_fmin_cobyla bfhzeta bfloat bfloatp bfpsi bfpsi0 bfzeta biconnected_components bimetric binomial bipartition block blockmatrixp bode_gain bode_phase bothcoef box boxplot boxplot_description break bug_report build_info|10 buildq build_sample burn cabs canform canten cardinality carg cartan cartesian_product catch cauchy_matrix cbffac cdf_bernoulli cdf_beta cdf_binomial cdf_cauchy cdf_chi2 cdf_continuous_uniform cdf_discrete_uniform cdf_exp cdf_f cdf_gamma cdf_general_finite_discrete cdf_geometric cdf_gumbel cdf_hypergeometric cdf_laplace cdf_logistic cdf_lognormal cdf_negative_binomial cdf_noncentral_chi2 cdf_noncentral_student_t cdf_normal cdf_pareto cdf_poisson cdf_rank_sum cdf_rayleigh cdf_signed_rank cdf_student_t cdf_weibull cdisplay ceiling central_moment cequal cequalignore cf cfdisrep cfexpand cgeodesic cgreaterp cgreaterpignore changename changevar chaosgame charat charfun charfun2 charlist charp charpoly chdir chebyshev_t chebyshev_u checkdiv check_overlaps chinese cholesky christof chromatic_index chromatic_number cint circulant_graph clear_edge_weight clear_rules clear_vertex_label clebsch_gordan clebsch_graph clessp clesspignore close closefile cmetric coeff coefmatrix cograd col collapse collectterms columnop columnspace columnswap columnvector combination combine comp2pui compare compfile compile compile_file complement_graph complete_bipartite_graph complete_graph complex_number_p components compose_functions concan concat conjugate conmetderiv connected_components connect_vertices cons constant constantp constituent constvalue cont2part content continuous_freq contortion contour_plot contract contract_edge contragrad contrib_ode convert coord copy copy_file copy_graph copylist copymatrix cor cos cosh cot coth cov cov1 covdiff covect covers crc24sum create_graph create_list csc csch csetup cspline ctaylor ct_coordsys ctransform ctranspose cube_graph cuboctahedron_graph cunlisp cv cycle_digraph cycle_graph cylindrical days360 dblint deactivate declare declare_constvalue declare_dimensions declare_fundamental_dimensions declare_fundamental_units declare_qty declare_translated declare_unit_conversion declare_units declare_weights decsym defcon define define_alt_display define_variable defint defmatch defrule defstruct deftaylor degree_sequence del delete deleten delta demo demoivre denom depends derivdegree derivlist describe desolve determinant dfloat dgauss_a dgauss_b dgeev dgemm dgeqrf dgesv dgesvd diag diagmatrix diag_matrix diagmatrixp diameter diff digitcharp dimacs_export dimacs_import dimension dimensionless dimensions dimensions_as_list direct directory discrete_freq disjoin disjointp disolate disp dispcon dispform dispfun dispJordan display disprule dispterms distrib divide divisors divsum dkummer_m dkummer_u dlange dodecahedron_graph dotproduct dotsimp dpart draw draw2d draw3d drawdf draw_file draw_graph dscalar echelon edge_coloring edge_connectivity edges eigens_by_jacobi eigenvalues eigenvectors eighth einstein eivals eivects elapsed_real_time elapsed_run_time ele2comp ele2polynome ele2pui elem elementp elevation_grid elim elim_allbut eliminate eliminate_using ellipse elliptic_e elliptic_ec elliptic_eu elliptic_f elliptic_kc elliptic_pi ematrix empty_graph emptyp endcons entermatrix entertensor entier equal equalp equiv_classes erf erfc erf_generalized erfi errcatch error errormsg errors euler ev eval_string evenp every evolution evolution2d evundiff example exp expand expandwrt expandwrt_factored expint expintegral_chi expintegral_ci expintegral_e expintegral_e1 expintegral_ei expintegral_e_simplify expintegral_li expintegral_shi expintegral_si explicit explose exponentialize express expt exsec extdiff extract_linear_equations extremal_subset ezgcd %f f90 facsum factcomb factor factorfacsum factorial factorout factorsum facts fast_central_elements fast_linsolve fasttimes featurep fernfale fft fib fibtophi fifth filename_merge file_search file_type fillarray findde find_root find_root_abs find_root_error find_root_rel first fix flatten flength float floatnump floor flower_snark flush flush1deriv flushd flushnd flush_output fmin_cobyla forget fortran fourcos fourexpand fourier fourier_elim fourint fourintcos fourintsin foursimp foursin fourth fposition frame_bracket freeof freshline fresnel_c fresnel_s from_adjacency_matrix frucht_graph full_listify fullmap fullmapl fullratsimp fullratsubst fullsetify funcsolve fundamental_dimensions fundamental_units fundef funmake funp fv g0 g1 gamma gamma_greek gamma_incomplete gamma_incomplete_generalized gamma_incomplete_regularized gauss gauss_a gauss_b gaussprob gcd gcdex gcdivide gcfac gcfactor gd generalized_lambert_w genfact gen_laguerre genmatrix gensym geo_amortization geo_annuity_fv geo_annuity_pv geomap geometric geometric_mean geosum get getcurrentdirectory get_edge_weight getenv get_lu_factors get_output_stream_string get_pixel get_plot_option get_tex_environment get_tex_environment_default get_vertex_label gfactor gfactorsum ggf girth global_variances gn gnuplot_close gnuplot_replot gnuplot_reset gnuplot_restart gnuplot_start go Gosper GosperSum gr2d gr3d gradef gramschmidt graph6_decode graph6_encode graph6_export graph6_import graph_center graph_charpoly graph_eigenvalues graph_flow graph_order graph_periphery graph_product graph_size graph_union great_rhombicosidodecahedron_graph great_rhombicuboctahedron_graph grid_graph grind grobner_basis grotzch_graph hamilton_cycle hamilton_path hankel hankel_1 hankel_2 harmonic harmonic_mean hav heawood_graph hermite hessian hgfred hilbertmap hilbert_matrix hipow histogram histogram_description hodge horner hypergeometric i0 i1 %ibes ic1 ic2 ic_convert ichr1 ichr2 icosahedron_graph icosidodecahedron_graph icurvature ident identfor identity idiff idim idummy ieqn %if ifactors iframes ifs igcdex igeodesic_coords ilt image imagpart imetric implicit implicit_derivative implicit_plot indexed_tensor indices induced_subgraph inferencep inference_result infix info_display init_atensor init_ctensor in_neighbors innerproduct inpart inprod inrt integerp integer_partitions integrate intersect intersection intervalp intopois intosum invariant1 invariant2 inverse_fft inverse_jacobi_cd inverse_jacobi_cn inverse_jacobi_cs inverse_jacobi_dc inverse_jacobi_dn inverse_jacobi_ds inverse_jacobi_nc inverse_jacobi_nd inverse_jacobi_ns inverse_jacobi_sc inverse_jacobi_sd inverse_jacobi_sn invert invert_by_adjoint invert_by_lu inv_mod irr is is_biconnected is_bipartite is_connected is_digraph is_edge_in_graph is_graph is_graph_or_digraph ishow is_isomorphic isolate isomorphism is_planar isqrt isreal_p is_sconnected is_tree is_vertex_in_graph items_inference %j j0 j1 jacobi jacobian jacobi_cd jacobi_cn jacobi_cs jacobi_dc jacobi_dn jacobi_ds jacobi_nc jacobi_nd jacobi_ns jacobi_p jacobi_sc jacobi_sd jacobi_sn JF jn join jordan julia julia_set julia_sin %k kdels kdelta kill killcontext kostka kron_delta kronecker_product kummer_m kummer_u kurtosis kurtosis_bernoulli kurtosis_beta kurtosis_binomial kurtosis_chi2 kurtosis_continuous_uniform kurtosis_discrete_uniform kurtosis_exp kurtosis_f kurtosis_gamma kurtosis_general_finite_discrete kurtosis_geometric kurtosis_gumbel kurtosis_hypergeometric kurtosis_laplace kurtosis_logistic kurtosis_lognormal kurtosis_negative_binomial kurtosis_noncentral_chi2 kurtosis_noncentral_student_t kurtosis_normal kurtosis_pareto kurtosis_poisson kurtosis_rayleigh kurtosis_student_t kurtosis_weibull label labels lagrange laguerre lambda lambert_w laplace laplacian_matrix last lbfgs lc2kdt lcharp lc_l lcm lc_u ldefint ldisp ldisplay legendre_p legendre_q leinstein length let letrules letsimp levi_civita lfreeof lgtreillis lhs li liediff limit Lindstedt linear linearinterpol linear_program linear_regression line_graph linsolve listarray list_correlations listify list_matrix_entries list_nc_monomials listoftens listofvars listp lmax lmin load loadfile local locate_matrix_entry log logcontract log_gamma lopow lorentz_gauge lowercasep lpart lratsubst lreduce lriemann lsquares_estimates lsquares_estimates_approximate lsquares_estimates_exact lsquares_mse lsquares_residual_mse lsquares_residuals lsum ltreillis lu_backsub lucas lu_factor %m macroexpand macroexpand1 make_array makebox makefact makegamma make_graph make_level_picture makelist makeOrders make_poly_continent make_poly_country make_polygon make_random_state make_rgb_picture makeset make_string_input_stream make_string_output_stream make_transform mandelbrot mandelbrot_set map mapatom maplist matchdeclare matchfix mat_cond mat_fullunblocker mat_function mathml_display mat_norm matrix matrixmap matrixp matrix_size mattrace mat_trace mat_unblocker max max_clique max_degree max_flow maximize_lp max_independent_set max_matching maybe md5sum mean mean_bernoulli mean_beta mean_binomial mean_chi2 mean_continuous_uniform mean_deviation mean_discrete_uniform mean_exp mean_f mean_gamma mean_general_finite_discrete mean_geometric mean_gumbel mean_hypergeometric mean_laplace mean_logistic mean_lognormal mean_negative_binomial mean_noncentral_chi2 mean_noncentral_student_t mean_normal mean_pareto mean_poisson mean_rayleigh mean_student_t mean_weibull median median_deviation member mesh metricexpandall mgf1_sha1 min min_degree min_edge_cut minfactorial minimalPoly minimize_lp minimum_spanning_tree minor minpack_lsquares minpack_solve min_vertex_cover min_vertex_cut mkdir mnewton mod mode_declare mode_identity ModeMatrix moebius mon2schur mono monomial_dimensions multibernstein_poly multi_display_for_texinfo multi_elem multinomial multinomial_coeff multi_orbit multiplot_mode multi_pui multsym multthru mycielski_graph nary natural_unit nc_degree ncexpt ncharpoly negative_picture neighbors new newcontext newdet new_graph newline newton new_variable next_prime nicedummies niceindices ninth nofix nonarray noncentral_moment nonmetricity nonnegintegerp nonscalarp nonzeroandfreeof notequal nounify nptetrad npv nroots nterms ntermst nthroot nullity nullspace num numbered_boundaries numberp number_to_octets num_distinct_partitions numerval numfactor num_partitions nusum nzeta nzetai nzetar octets_to_number octets_to_oid odd_girth oddp ode2 ode_check odelin oid_to_octets op opena opena_binary openr openr_binary openw openw_binary operatorp opsubst optimize %or orbit orbits ordergreat ordergreatp orderless orderlessp orthogonal_complement orthopoly_recur orthopoly_weight outermap out_neighbors outofpois pade parabolic_cylinder_d parametric parametric_surface parg parGosper parse_string parse_timedate part part2cont partfrac partition partition_set partpol path_digraph path_graph pathname_directory pathname_name pathname_type pdf_bernoulli pdf_beta pdf_binomial pdf_cauchy pdf_chi2 pdf_continuous_uniform pdf_discrete_uniform pdf_exp pdf_f pdf_gamma pdf_general_finite_discrete pdf_geometric pdf_gumbel pdf_hypergeometric pdf_laplace pdf_logistic pdf_lognormal pdf_negative_binomial pdf_noncentral_chi2 pdf_noncentral_student_t pdf_normal pdf_pareto pdf_poisson pdf_rank_sum pdf_rayleigh pdf_signed_rank pdf_student_t pdf_weibull pearson_skewness permanent permut permutation permutations petersen_graph petrov pickapart picture_equalp picturep piechart piechart_description planar_embedding playback plog plot2d plot3d plotdf ploteq plsquares pochhammer points poisdiff poisexpt poisint poismap poisplus poissimp poissubst poistimes poistrim polar polarform polartorect polar_to_xy poly_add poly_buchberger poly_buchberger_criterion poly_colon_ideal poly_content polydecomp poly_depends_p poly_elimination_ideal poly_exact_divide poly_expand poly_expt poly_gcd polygon poly_grobner poly_grobner_equal poly_grobner_member poly_grobner_subsetp poly_ideal_intersection poly_ideal_polysaturation poly_ideal_polysaturation1 poly_ideal_saturation poly_ideal_saturation1 poly_lcm poly_minimization polymod poly_multiply polynome2ele polynomialp poly_normal_form poly_normalize poly_normalize_list poly_polysaturation_extension poly_primitive_part poly_pseudo_divide poly_reduced_grobner poly_reduction poly_saturation_extension poly_s_polynomial poly_subtract polytocompanion pop postfix potential power_mod powerseries powerset prefix prev_prime primep primes principal_components print printf printfile print_graph printpois printprops prodrac product properties propvars psi psubst ptriangularize pui pui2comp pui2ele pui2polynome pui_direct puireduc push put pv qput qrange qty quad_control quad_qag quad_qagi quad_qagp quad_qags quad_qawc quad_qawf quad_qawo quad_qaws quadrilateral quantile quantile_bernoulli quantile_beta quantile_binomial quantile_cauchy quantile_chi2 quantile_continuous_uniform quantile_discrete_uniform quantile_exp quantile_f quantile_gamma quantile_general_finite_discrete quantile_geometric quantile_gumbel quantile_hypergeometric quantile_laplace quantile_logistic quantile_lognormal quantile_negative_binomial quantile_noncentral_chi2 quantile_noncentral_student_t quantile_normal quantile_pareto quantile_poisson quantile_rayleigh quantile_student_t quantile_weibull quartile_skewness quit qunit quotient racah_v racah_w radcan radius random random_bernoulli random_beta random_binomial random_bipartite_graph random_cauchy random_chi2 random_continuous_uniform random_digraph random_discrete_uniform random_exp random_f random_gamma random_general_finite_discrete random_geometric random_graph random_graph1 random_gumbel random_hypergeometric random_laplace random_logistic random_lognormal random_negative_binomial random_network random_noncentral_chi2 random_noncentral_student_t random_normal random_pareto random_permutation random_poisson random_rayleigh random_regular_graph random_student_t random_tournament random_tree random_weibull range rank rat ratcoef ratdenom ratdiff ratdisrep ratexpand ratinterpol rational rationalize ratnumer ratnump ratp ratsimp ratsubst ratvars ratweight read read_array read_binary_array read_binary_list read_binary_matrix readbyte readchar read_hashed_array readline read_list read_matrix read_nested_list readonly read_xpm real_imagpart_to_conjugate realpart realroots rearray rectangle rectform rectform_log_if_constant recttopolar rediff reduce_consts reduce_order region region_boundaries region_boundaries_plus rem remainder remarray rembox remcomps remcon remcoord remfun remfunction remlet remove remove_constvalue remove_dimensions remove_edge remove_fundamental_dimensions remove_fundamental_units remove_plot_option remove_vertex rempart remrule remsym remvalue rename rename_file reset reset_displays residue resolvante resolvante_alternee1 resolvante_bipartite resolvante_diedrale resolvante_klein resolvante_klein3 resolvante_produit_sym resolvante_unitaire resolvante_vierer rest resultant return reveal reverse revert revert2 rgb2level rhs ricci riemann rinvariant risch rk rmdir rncombine romberg room rootscontract round row rowop rowswap rreduce run_testsuite %s save saving scalarp scaled_bessel_i scaled_bessel_i0 scaled_bessel_i1 scalefactors scanmap scatterplot scatterplot_description scene schur2comp sconcat scopy scsimp scurvature sdowncase sec sech second sequal sequalignore set_alt_display setdifference set_draw_defaults set_edge_weight setelmx setequalp setify setp set_partitions set_plot_option set_prompt set_random_state set_tex_environment set_tex_environment_default setunits setup_autoload set_up_dot_simplifications set_vertex_label seventh sexplode sf sha1sum sha256sum shortest_path shortest_weighted_path show showcomps showratvars sierpinskiale sierpinskimap sign signum similaritytransform simp_inequality simplify_sum simplode simpmetderiv simtran sin sinh sinsert sinvertcase sixth skewness skewness_bernoulli skewness_beta skewness_binomial skewness_chi2 skewness_continuous_uniform skewness_discrete_uniform skewness_exp skewness_f skewness_gamma skewness_general_finite_discrete skewness_geometric skewness_gumbel skewness_hypergeometric skewness_laplace skewness_logistic skewness_lognormal skewness_negative_binomial skewness_noncentral_chi2 skewness_noncentral_student_t skewness_normal skewness_pareto skewness_poisson skewness_rayleigh skewness_student_t skewness_weibull slength smake small_rhombicosidodecahedron_graph small_rhombicuboctahedron_graph smax smin smismatch snowmap snub_cube_graph snub_dodecahedron_graph solve solve_rec solve_rec_rat some somrac sort sparse6_decode sparse6_encode sparse6_export sparse6_import specint spherical spherical_bessel_j spherical_bessel_y spherical_hankel1 spherical_hankel2 spherical_harmonic spherical_to_xyz splice split sposition sprint sqfr sqrt sqrtdenest sremove sremovefirst sreverse ssearch ssort sstatus ssubst ssubstfirst staircase standardize standardize_inverse_trig starplot starplot_description status std std1 std_bernoulli std_beta std_binomial std_chi2 std_continuous_uniform std_discrete_uniform std_exp std_f std_gamma std_general_finite_discrete std_geometric std_gumbel std_hypergeometric std_laplace std_logistic std_lognormal std_negative_binomial std_noncentral_chi2 std_noncentral_student_t std_normal std_pareto std_poisson std_rayleigh std_student_t std_weibull stemplot stirling stirling1 stirling2 strim striml strimr string stringout stringp strong_components struve_h struve_l sublis sublist sublist_indices submatrix subsample subset subsetp subst substinpart subst_parallel substpart substring subvar subvarp sum sumcontract summand_to_rec supcase supcontext symbolp symmdifference symmetricp system take_channel take_inference tan tanh taylor taylorinfo taylorp taylor_simplifier taytorat tcl_output tcontract tellrat tellsimp tellsimpafter tentex tenth test_mean test_means_difference test_normality test_proportion test_proportions_difference test_rank_sum test_sign test_signed_rank test_variance test_variance_ratio tex tex1 tex_display texput %th third throw time timedate timer timer_info tldefint tlimit todd_coxeter toeplitz tokens to_lisp topological_sort to_poly to_poly_solve totaldisrep totalfourier totient tpartpol trace tracematrix trace_options transform_sample translate translate_file transpose treefale tree_reduce treillis treinat triangle triangularize trigexpand trigrat trigreduce trigsimp trunc truncate truncated_cube_graph truncated_dodecahedron_graph truncated_icosahedron_graph truncated_tetrahedron_graph tr_warnings_get tube tutte_graph ueivects uforget ultraspherical underlying_graph undiff union unique uniteigenvectors unitp units unit_step unitvector unorder unsum untellrat untimer untrace uppercasep uricci uriemann uvect vandermonde_matrix var var1 var_bernoulli var_beta var_binomial var_chi2 var_continuous_uniform var_discrete_uniform var_exp var_f var_gamma var_general_finite_discrete var_geometric var_gumbel var_hypergeometric var_laplace var_logistic var_lognormal var_negative_binomial var_noncentral_chi2 var_noncentral_student_t var_normal var_pareto var_poisson var_rayleigh var_student_t var_weibull vector vectorpotential vectorsimp verbify vers vertex_coloring vertex_connectivity vertex_degree vertex_distance vertex_eccentricity vertex_in_degree vertex_out_degree vertices vertices_to_cycle vertices_to_path %w weyl wheel_graph wiener_index wigner_3j wigner_6j wigner_9j with_stdout write_binary_data writebyte write_data writefile wronskian xreduce xthru %y Zeilberger zeroequiv zerofor zeromatrix zeromatrixp zeta zgeev zheev zlange zn_add_table zn_carmichael_lambda zn_characteristic_factors zn_determinant zn_factor_generators zn_invert_by_lu zn_log zn_mult_table absboxchar activecontexts adapt_depth additive adim aform algebraic algepsilon algexact aliases allbut all_dotsimp_denoms allocation allsym alphabetic animation antisymmetric arrays askexp assume_pos assume_pos_pred assumescalar asymbol atomgrad atrig1 axes axis_3d axis_bottom axis_left axis_right axis_top azimuth background background_color backsubst berlefact bernstein_explicit besselexpand beta_args_sum_to_integer beta_expand bftorat bftrunc bindtest border boundaries_array box boxchar breakup %c capping cauchysum cbrange cbtics center cflength cframe_flag cnonmet_flag color color_bar color_bar_tics colorbox columns commutative complex cone context contexts contour contour_levels cosnpiflag ctaypov ctaypt ctayswitch ctayvar ct_coords ctorsion_flag ctrgsimp cube current_let_rule_package cylinder data_file_name debugmode decreasing default_let_rule_package delay dependencies derivabbrev derivsubst detout diagmetric diff dim dimensions dispflag display2d|10 display_format_internal distribute_over doallmxops domain domxexpt domxmxops domxnctimes dontfactor doscmxops doscmxplus dot0nscsimp dot0simp dot1simp dotassoc dotconstrules dotdistrib dotexptsimp dotident dotscrules draw_graph_program draw_realpart edge_color edge_coloring edge_partition edge_type edge_width %edispflag elevation %emode endphi endtheta engineering_format_floats enhanced3d %enumer epsilon_lp erfflag erf_representation errormsg error_size error_syms error_type %e_to_numlog eval even evenfun evflag evfun ev_point expandwrt_denom expintexpand expintrep expon expop exptdispflag exptisolate exptsubst facexpand facsum_combine factlim factorflag factorial_expand factors_only fb feature features file_name file_output_append file_search_demo file_search_lisp file_search_maxima|10 file_search_tests file_search_usage file_type_lisp file_type_maxima|10 fill_color fill_density filled_func fixed_vertices flipflag float2bf font font_size fortindent fortspaces fpprec fpprintprec functions gamma_expand gammalim gdet genindex gensumnum GGFCFMAX GGFINFINITY globalsolve gnuplot_command gnuplot_curve_styles gnuplot_curve_titles gnuplot_default_term_command gnuplot_dumb_term_command gnuplot_file_args gnuplot_file_name gnuplot_out_file gnuplot_pdf_term_command gnuplot_pm3d gnuplot_png_term_command gnuplot_postamble gnuplot_preamble gnuplot_ps_term_command gnuplot_svg_term_command gnuplot_term gnuplot_view_args Gosper_in_Zeilberger gradefs grid grid2d grind halfangles head_angle head_both head_length head_type height hypergeometric_representation %iargs ibase icc1 icc2 icounter idummyx ieqnprint ifb ifc1 ifc2 ifg ifgi ifr iframe_bracket_form ifri igeowedge_flag ikt1 ikt2 imaginary inchar increasing infeval infinity inflag infolists inm inmc1 inmc2 intanalysis integer integervalued integrate_use_rootsof integration_constant integration_constant_counter interpolate_color intfaclim ip_grid ip_grid_in irrational isolate_wrt_times iterations itr julia_parameter %k1 %k2 keepfloat key key_pos kinvariant kt label label_alignment label_orientation labels lassociative lbfgs_ncorrections lbfgs_nfeval_max leftjust legend letrat let_rule_packages lfg lg lhospitallim limsubst linear linear_solver linechar linel|10 linenum line_type linewidth line_width linsolve_params linsolvewarn lispdisp listarith listconstvars listdummyvars lmxchar load_pathname loadprint logabs logarc logcb logconcoeffp logexpand lognegint logsimp logx logx_secondary logy logy_secondary logz lriem m1pbranch macroexpansion macros mainvar manual_demo maperror mapprint matrix_element_add matrix_element_mult matrix_element_transpose maxapplydepth maxapplyheight maxima_tempdir|10 maxima_userdir|10 maxnegex MAX_ORD maxposex maxpsifracdenom maxpsifracnum maxpsinegint maxpsiposint maxtayorder mesh_lines_color method mod_big_prime mode_check_errorp mode_checkp mode_check_warnp mod_test mod_threshold modular_linear_solver modulus multiplicative multiplicities myoptions nary negdistrib negsumdispflag newline newtonepsilon newtonmaxiter nextlayerfactor niceindicespref nm nmc noeval nolabels nonegative_lp noninteger nonscalar noun noundisp nouns np npi nticks ntrig numer numer_pbranch obase odd oddfun opacity opproperties opsubst optimprefix optionset orientation origin orthopoly_returns_intervals outative outchar packagefile palette partswitch pdf_file pfeformat phiresolution %piargs piece pivot_count_sx pivot_max_sx plot_format plot_options plot_realpart png_file pochhammer_max_index points pointsize point_size points_joined point_type poislim poisson poly_coefficient_ring poly_elimination_order polyfactor poly_grobner_algorithm poly_grobner_debug poly_monomial_order poly_primary_elimination_order poly_return_term_list poly_secondary_elimination_order poly_top_reduction_only posfun position powerdisp pred prederror primep_number_of_tests product_use_gamma program programmode promote_float_to_bigfloat prompt proportional_axes props psexpand ps_file radexpand radius radsubstflag rassociative ratalgdenom ratchristof ratdenomdivide rateinstein ratepsilon ratfac rational ratmx ratprint ratriemann ratsimpexpons ratvarswitch ratweights ratweyl ratwtlvl real realonly redraw refcheck resolution restart resultant ric riem rmxchar %rnum_list rombergabs rombergit rombergmin rombergtol rootsconmode rootsepsilon run_viewer same_xy same_xyz savedef savefactors scalar scalarmatrixp scale scale_lp setcheck setcheckbreak setval show_edge_color show_edges show_edge_type show_edge_width show_id show_label showtime show_vertex_color show_vertex_size show_vertex_type show_vertices show_weight simp simplified_output simplify_products simpproduct simpsum sinnpiflag solvedecomposes solveexplicit solvefactors solvenullwarn solveradcan solvetrigwarn space sparse sphere spring_embedding_depth sqrtdispflag stardisp startphi starttheta stats_numer stringdisp structures style sublis_apply_lambda subnumsimp sumexpand sumsplitfact surface surface_hide svg_file symmetric tab taylordepth taylor_logexpand taylor_order_coefficients taylor_truncate_polynomials tensorkill terminal testsuite_files thetaresolution timer_devalue title tlimswitch tr track transcompile transform transform_xy translate_fast_arrays transparent transrun tr_array_as_ref tr_bound_function_applyp tr_file_tty_messagesp tr_float_can_branch_complex tr_function_call_default trigexpandplus trigexpandtimes triginverses trigsign trivial_solutions tr_numer tr_optimize_max_loop tr_semicompile tr_state_vars tr_warn_bad_function_calls tr_warn_fexpr tr_warn_meval tr_warn_mode tr_warn_undeclared tr_warn_undefined_variable tstep ttyoff tube_extremes ufg ug %unitexpand unit_vectors uric uriem use_fast_arrays user_preamble usersetunits values vect_cross verbose vertex_color vertex_coloring vertex_partition vertex_size vertex_type view warnings weyl width windowname windowtitle wired_surface wireframe xaxis xaxis_color xaxis_secondary xaxis_type xaxis_width xlabel xlabel_secondary xlength xrange xrange_secondary xtics xtics_axis xtics_rotate xtics_rotate_secondary xtics_secondary xtics_secondary_axis xu_grid x_voxel xy_file xyplane xy_scale yaxis yaxis_color yaxis_secondary yaxis_type yaxis_width ylabel ylabel_secondary ylength yrange yrange_secondary ytics ytics_axis ytics_rotate ytics_rotate_secondary ytics_secondary ytics_secondary_axis yv_grid y_voxel yx_ratio zaxis zaxis_color zaxis_type zaxis_width zeroa zerob zerobern zeta%pi zlabel zlabel_rotate zlength zmin zn_primroot_limit zn_primroot_pretest",
+contains:[{className:"comment",begin:/\(\*/,end:/\*\)/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{begin:/\{/,end:/\}/,illegal:/:/}]}});b.registerLanguage("matlab",function(a){var b={relevance:0,contains:[{begin:"('|\\.')+"}]};return{keywords:{keyword:"break case catch classdef continue else elseif end enumerated events for function global if methods otherwise parfor persistent properties return spmd switch try while",built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun legend intersect ismember procrustes hold num2cell "},
+illegal:'(//|"|#|/\\*|\\s+/\\w+)',contains:[{className:"function",beginKeywords:"function",end:"$",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}]}]},{className:"built_in",begin:/true|false/,relevance:0,starts:b},{begin:"[a-zA-Z][a-zA-Z_0-9]*('|\\.')+",relevance:0},{className:"number",begin:a.C_NUMBER_RE,relevance:0,starts:b},{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{begin:/\]|}|\)/,relevance:0,
+starts:b},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}],starts:b},a.COMMENT("^\\s*\\%\\{\\s*$","^\\s*\\%\\}\\s*$"),a.COMMENT("\\%","$")]}});b.registerLanguage("maxima",function(a){return{lexemes:"[A-Za-z_%][0-9A-Za-z_%]*",keywords:{keyword:"if then else elseif for thru do while unless step in and or not",literal:"true false unknown inf minf ind und %e %i %pi %phi %gamma",built_in:" abasep abs absint absolute_real_time acos acosh acot acoth acsc acsch activate addcol add_edge add_edges addmatrices addrow add_vertex add_vertices adjacency_matrix adjoin adjoint af agd airy airy_ai airy_bi airy_dai airy_dbi algsys alg_type alias allroots alphacharp alphanumericp amortization %and annuity_fv annuity_pv antid antidiff AntiDifference append appendfile apply apply1 apply2 applyb1 apropos args arit_amortization arithmetic arithsum array arrayapply arrayinfo arraymake arraysetapply ascii asec asech asin asinh askinteger asksign assoc assoc_legendre_p assoc_legendre_q assume assume_external_byte_order asympa at atan atan2 atanh atensimp atom atvalue augcoefmatrix augmented_lagrangian_method av average_degree backtrace bars barsplot barsplot_description base64 base64_decode bashindices batch batchload bc2 bdvac belln benefit_cost bern bernpoly bernstein_approx bernstein_expand bernstein_poly bessel bessel_i bessel_j bessel_k bessel_simplify bessel_y beta beta_incomplete beta_incomplete_generalized beta_incomplete_regularized bezout bfallroots bffac bf_find_root bf_fmin_cobyla bfhzeta bfloat bfloatp bfpsi bfpsi0 bfzeta biconnected_components bimetric binomial bipartition block blockmatrixp bode_gain bode_phase bothcoef box boxplot boxplot_description break bug_report build_info|10 buildq build_sample burn cabs canform canten cardinality carg cartan cartesian_product catch cauchy_matrix cbffac cdf_bernoulli cdf_beta cdf_binomial cdf_cauchy cdf_chi2 cdf_continuous_uniform cdf_discrete_uniform cdf_exp cdf_f cdf_gamma cdf_general_finite_discrete cdf_geometric cdf_gumbel cdf_hypergeometric cdf_laplace cdf_logistic cdf_lognormal cdf_negative_binomial cdf_noncentral_chi2 cdf_noncentral_student_t cdf_normal cdf_pareto cdf_poisson cdf_rank_sum cdf_rayleigh cdf_signed_rank cdf_student_t cdf_weibull cdisplay ceiling central_moment cequal cequalignore cf cfdisrep cfexpand cgeodesic cgreaterp cgreaterpignore changename changevar chaosgame charat charfun charfun2 charlist charp charpoly chdir chebyshev_t chebyshev_u checkdiv check_overlaps chinese cholesky christof chromatic_index chromatic_number cint circulant_graph clear_edge_weight clear_rules clear_vertex_label clebsch_gordan clebsch_graph clessp clesspignore close closefile cmetric coeff coefmatrix cograd col collapse collectterms columnop columnspace columnswap columnvector combination combine comp2pui compare compfile compile compile_file complement_graph complete_bipartite_graph complete_graph complex_number_p components compose_functions concan concat conjugate conmetderiv connected_components connect_vertices cons constant constantp constituent constvalue cont2part content continuous_freq contortion contour_plot contract contract_edge contragrad contrib_ode convert coord copy copy_file copy_graph copylist copymatrix cor cos cosh cot coth cov cov1 covdiff covect covers crc24sum create_graph create_list csc csch csetup cspline ctaylor ct_coordsys ctransform ctranspose cube_graph cuboctahedron_graph cunlisp cv cycle_digraph cycle_graph cylindrical days360 dblint deactivate declare declare_constvalue declare_dimensions declare_fundamental_dimensions declare_fundamental_units declare_qty declare_translated declare_unit_conversion declare_units declare_weights decsym defcon define define_alt_display define_variable defint defmatch defrule defstruct deftaylor degree_sequence del delete deleten delta demo demoivre denom depends derivdegree derivlist describe desolve determinant dfloat dgauss_a dgauss_b dgeev dgemm dgeqrf dgesv dgesvd diag diagmatrix diag_matrix diagmatrixp diameter diff digitcharp dimacs_export dimacs_import dimension dimensionless dimensions dimensions_as_list direct directory discrete_freq disjoin disjointp disolate disp dispcon dispform dispfun dispJordan display disprule dispterms distrib divide divisors divsum dkummer_m dkummer_u dlange dodecahedron_graph dotproduct dotsimp dpart draw draw2d draw3d drawdf draw_file draw_graph dscalar echelon edge_coloring edge_connectivity edges eigens_by_jacobi eigenvalues eigenvectors eighth einstein eivals eivects elapsed_real_time elapsed_run_time ele2comp ele2polynome ele2pui elem elementp elevation_grid elim elim_allbut eliminate eliminate_using ellipse elliptic_e elliptic_ec elliptic_eu elliptic_f elliptic_kc elliptic_pi ematrix empty_graph emptyp endcons entermatrix entertensor entier equal equalp equiv_classes erf erfc erf_generalized erfi errcatch error errormsg errors euler ev eval_string evenp every evolution evolution2d evundiff example exp expand expandwrt expandwrt_factored expint expintegral_chi expintegral_ci expintegral_e expintegral_e1 expintegral_ei expintegral_e_simplify expintegral_li expintegral_shi expintegral_si explicit explose exponentialize express expt exsec extdiff extract_linear_equations extremal_subset ezgcd %f f90 facsum factcomb factor factorfacsum factorial factorout factorsum facts fast_central_elements fast_linsolve fasttimes featurep fernfale fft fib fibtophi fifth filename_merge file_search file_type fillarray findde find_root find_root_abs find_root_error find_root_rel first fix flatten flength float floatnump floor flower_snark flush flush1deriv flushd flushnd flush_output fmin_cobyla forget fortran fourcos fourexpand fourier fourier_elim fourint fourintcos fourintsin foursimp foursin fourth fposition frame_bracket freeof freshline fresnel_c fresnel_s from_adjacency_matrix frucht_graph full_listify fullmap fullmapl fullratsimp fullratsubst fullsetify funcsolve fundamental_dimensions fundamental_units fundef funmake funp fv g0 g1 gamma gamma_greek gamma_incomplete gamma_incomplete_generalized gamma_incomplete_regularized gauss gauss_a gauss_b gaussprob gcd gcdex gcdivide gcfac gcfactor gd generalized_lambert_w genfact gen_laguerre genmatrix gensym geo_amortization geo_annuity_fv geo_annuity_pv geomap geometric geometric_mean geosum get getcurrentdirectory get_edge_weight getenv get_lu_factors get_output_stream_string get_pixel get_plot_option get_tex_environment get_tex_environment_default get_vertex_label gfactor gfactorsum ggf girth global_variances gn gnuplot_close gnuplot_replot gnuplot_reset gnuplot_restart gnuplot_start go Gosper GosperSum gr2d gr3d gradef gramschmidt graph6_decode graph6_encode graph6_export graph6_import graph_center graph_charpoly graph_eigenvalues graph_flow graph_order graph_periphery graph_product graph_size graph_union great_rhombicosidodecahedron_graph great_rhombicuboctahedron_graph grid_graph grind grobner_basis grotzch_graph hamilton_cycle hamilton_path hankel hankel_1 hankel_2 harmonic harmonic_mean hav heawood_graph hermite hessian hgfred hilbertmap hilbert_matrix hipow histogram histogram_description hodge horner hypergeometric i0 i1 %ibes ic1 ic2 ic_convert ichr1 ichr2 icosahedron_graph icosidodecahedron_graph icurvature ident identfor identity idiff idim idummy ieqn %if ifactors iframes ifs igcdex igeodesic_coords ilt image imagpart imetric implicit implicit_derivative implicit_plot indexed_tensor indices induced_subgraph inferencep inference_result infix info_display init_atensor init_ctensor in_neighbors innerproduct inpart inprod inrt integerp integer_partitions integrate intersect intersection intervalp intopois intosum invariant1 invariant2 inverse_fft inverse_jacobi_cd inverse_jacobi_cn inverse_jacobi_cs inverse_jacobi_dc inverse_jacobi_dn inverse_jacobi_ds inverse_jacobi_nc inverse_jacobi_nd inverse_jacobi_ns inverse_jacobi_sc inverse_jacobi_sd inverse_jacobi_sn invert invert_by_adjoint invert_by_lu inv_mod irr is is_biconnected is_bipartite is_connected is_digraph is_edge_in_graph is_graph is_graph_or_digraph ishow is_isomorphic isolate isomorphism is_planar isqrt isreal_p is_sconnected is_tree is_vertex_in_graph items_inference %j j0 j1 jacobi jacobian jacobi_cd jacobi_cn jacobi_cs jacobi_dc jacobi_dn jacobi_ds jacobi_nc jacobi_nd jacobi_ns jacobi_p jacobi_sc jacobi_sd jacobi_sn JF jn join jordan julia julia_set julia_sin %k kdels kdelta kill killcontext kostka kron_delta kronecker_product kummer_m kummer_u kurtosis kurtosis_bernoulli kurtosis_beta kurtosis_binomial kurtosis_chi2 kurtosis_continuous_uniform kurtosis_discrete_uniform kurtosis_exp kurtosis_f kurtosis_gamma kurtosis_general_finite_discrete kurtosis_geometric kurtosis_gumbel kurtosis_hypergeometric kurtosis_laplace kurtosis_logistic kurtosis_lognormal kurtosis_negative_binomial kurtosis_noncentral_chi2 kurtosis_noncentral_student_t kurtosis_normal kurtosis_pareto kurtosis_poisson kurtosis_rayleigh kurtosis_student_t kurtosis_weibull label labels lagrange laguerre lambda lambert_w laplace laplacian_matrix last lbfgs lc2kdt lcharp lc_l lcm lc_u ldefint ldisp ldisplay legendre_p legendre_q leinstein length let letrules letsimp levi_civita lfreeof lgtreillis lhs li liediff limit Lindstedt linear linearinterpol linear_program linear_regression line_graph linsolve listarray list_correlations listify list_matrix_entries list_nc_monomials listoftens listofvars listp lmax lmin load loadfile local locate_matrix_entry log logcontract log_gamma lopow lorentz_gauge lowercasep lpart lratsubst lreduce lriemann lsquares_estimates lsquares_estimates_approximate lsquares_estimates_exact lsquares_mse lsquares_residual_mse lsquares_residuals lsum ltreillis lu_backsub lucas lu_factor %m macroexpand macroexpand1 make_array makebox makefact makegamma make_graph make_level_picture makelist makeOrders make_poly_continent make_poly_country make_polygon make_random_state make_rgb_picture makeset make_string_input_stream make_string_output_stream make_transform mandelbrot mandelbrot_set map mapatom maplist matchdeclare matchfix mat_cond mat_fullunblocker mat_function mathml_display mat_norm matrix matrixmap matrixp matrix_size mattrace mat_trace mat_unblocker max max_clique max_degree max_flow maximize_lp max_independent_set max_matching maybe md5sum mean mean_bernoulli mean_beta mean_binomial mean_chi2 mean_continuous_uniform mean_deviation mean_discrete_uniform mean_exp mean_f mean_gamma mean_general_finite_discrete mean_geometric mean_gumbel mean_hypergeometric mean_laplace mean_logistic mean_lognormal mean_negative_binomial mean_noncentral_chi2 mean_noncentral_student_t mean_normal mean_pareto mean_poisson mean_rayleigh mean_student_t mean_weibull median median_deviation member mesh metricexpandall mgf1_sha1 min min_degree min_edge_cut minfactorial minimalPoly minimize_lp minimum_spanning_tree minor minpack_lsquares minpack_solve min_vertex_cover min_vertex_cut mkdir mnewton mod mode_declare mode_identity ModeMatrix moebius mon2schur mono monomial_dimensions multibernstein_poly multi_display_for_texinfo multi_elem multinomial multinomial_coeff multi_orbit multiplot_mode multi_pui multsym multthru mycielski_graph nary natural_unit nc_degree ncexpt ncharpoly negative_picture neighbors new newcontext newdet new_graph newline newton new_variable next_prime nicedummies niceindices ninth nofix nonarray noncentral_moment nonmetricity nonnegintegerp nonscalarp nonzeroandfreeof notequal nounify nptetrad npv nroots nterms ntermst nthroot nullity nullspace num numbered_boundaries numberp number_to_octets num_distinct_partitions numerval numfactor num_partitions nusum nzeta nzetai nzetar octets_to_number octets_to_oid odd_girth oddp ode2 ode_check odelin oid_to_octets op opena opena_binary openr openr_binary openw openw_binary operatorp opsubst optimize %or orbit orbits ordergreat ordergreatp orderless orderlessp orthogonal_complement orthopoly_recur orthopoly_weight outermap out_neighbors outofpois pade parabolic_cylinder_d parametric parametric_surface parg parGosper parse_string parse_timedate part part2cont partfrac partition partition_set partpol path_digraph path_graph pathname_directory pathname_name pathname_type pdf_bernoulli pdf_beta pdf_binomial pdf_cauchy pdf_chi2 pdf_continuous_uniform pdf_discrete_uniform pdf_exp pdf_f pdf_gamma pdf_general_finite_discrete pdf_geometric pdf_gumbel pdf_hypergeometric pdf_laplace pdf_logistic pdf_lognormal pdf_negative_binomial pdf_noncentral_chi2 pdf_noncentral_student_t pdf_normal pdf_pareto pdf_poisson pdf_rank_sum pdf_rayleigh pdf_signed_rank pdf_student_t pdf_weibull pearson_skewness permanent permut permutation permutations petersen_graph petrov pickapart picture_equalp picturep piechart piechart_description planar_embedding playback plog plot2d plot3d plotdf ploteq plsquares pochhammer points poisdiff poisexpt poisint poismap poisplus poissimp poissubst poistimes poistrim polar polarform polartorect polar_to_xy poly_add poly_buchberger poly_buchberger_criterion poly_colon_ideal poly_content polydecomp poly_depends_p poly_elimination_ideal poly_exact_divide poly_expand poly_expt poly_gcd polygon poly_grobner poly_grobner_equal poly_grobner_member poly_grobner_subsetp poly_ideal_intersection poly_ideal_polysaturation poly_ideal_polysaturation1 poly_ideal_saturation poly_ideal_saturation1 poly_lcm poly_minimization polymod poly_multiply polynome2ele polynomialp poly_normal_form poly_normalize poly_normalize_list poly_polysaturation_extension poly_primitive_part poly_pseudo_divide poly_reduced_grobner poly_reduction poly_saturation_extension poly_s_polynomial poly_subtract polytocompanion pop postfix potential power_mod powerseries powerset prefix prev_prime primep primes principal_components print printf printfile print_graph printpois printprops prodrac product properties propvars psi psubst ptriangularize pui pui2comp pui2ele pui2polynome pui_direct puireduc push put pv qput qrange qty quad_control quad_qag quad_qagi quad_qagp quad_qags quad_qawc quad_qawf quad_qawo quad_qaws quadrilateral quantile quantile_bernoulli quantile_beta quantile_binomial quantile_cauchy quantile_chi2 quantile_continuous_uniform quantile_discrete_uniform quantile_exp quantile_f quantile_gamma quantile_general_finite_discrete quantile_geometric quantile_gumbel quantile_hypergeometric quantile_laplace quantile_logistic quantile_lognormal quantile_negative_binomial quantile_noncentral_chi2 quantile_noncentral_student_t quantile_normal quantile_pareto quantile_poisson quantile_rayleigh quantile_student_t quantile_weibull quartile_skewness quit qunit quotient racah_v racah_w radcan radius random random_bernoulli random_beta random_binomial random_bipartite_graph random_cauchy random_chi2 random_continuous_uniform random_digraph random_discrete_uniform random_exp random_f random_gamma random_general_finite_discrete random_geometric random_graph random_graph1 random_gumbel random_hypergeometric random_laplace random_logistic random_lognormal random_negative_binomial random_network random_noncentral_chi2 random_noncentral_student_t random_normal random_pareto random_permutation random_poisson random_rayleigh random_regular_graph random_student_t random_tournament random_tree random_weibull range rank rat ratcoef ratdenom ratdiff ratdisrep ratexpand ratinterpol rational rationalize ratnumer ratnump ratp ratsimp ratsubst ratvars ratweight read read_array read_binary_array read_binary_list read_binary_matrix readbyte readchar read_hashed_array readline read_list read_matrix read_nested_list readonly read_xpm real_imagpart_to_conjugate realpart realroots rearray rectangle rectform rectform_log_if_constant recttopolar rediff reduce_consts reduce_order region region_boundaries region_boundaries_plus rem remainder remarray rembox remcomps remcon remcoord remfun remfunction remlet remove remove_constvalue remove_dimensions remove_edge remove_fundamental_dimensions remove_fundamental_units remove_plot_option remove_vertex rempart remrule remsym remvalue rename rename_file reset reset_displays residue resolvante resolvante_alternee1 resolvante_bipartite resolvante_diedrale resolvante_klein resolvante_klein3 resolvante_produit_sym resolvante_unitaire resolvante_vierer rest resultant return reveal reverse revert revert2 rgb2level rhs ricci riemann rinvariant risch rk rmdir rncombine romberg room rootscontract round row rowop rowswap rreduce run_testsuite %s save saving scalarp scaled_bessel_i scaled_bessel_i0 scaled_bessel_i1 scalefactors scanmap scatterplot scatterplot_description scene schur2comp sconcat scopy scsimp scurvature sdowncase sec sech second sequal sequalignore set_alt_display setdifference set_draw_defaults set_edge_weight setelmx setequalp setify setp set_partitions set_plot_option set_prompt set_random_state set_tex_environment set_tex_environment_default setunits setup_autoload set_up_dot_simplifications set_vertex_label seventh sexplode sf sha1sum sha256sum shortest_path shortest_weighted_path show showcomps showratvars sierpinskiale sierpinskimap sign signum similaritytransform simp_inequality simplify_sum simplode simpmetderiv simtran sin sinh sinsert sinvertcase sixth skewness skewness_bernoulli skewness_beta skewness_binomial skewness_chi2 skewness_continuous_uniform skewness_discrete_uniform skewness_exp skewness_f skewness_gamma skewness_general_finite_discrete skewness_geometric skewness_gumbel skewness_hypergeometric skewness_laplace skewness_logistic skewness_lognormal skewness_negative_binomial skewness_noncentral_chi2 skewness_noncentral_student_t skewness_normal skewness_pareto skewness_poisson skewness_rayleigh skewness_student_t skewness_weibull slength smake small_rhombicosidodecahedron_graph small_rhombicuboctahedron_graph smax smin smismatch snowmap snub_cube_graph snub_dodecahedron_graph solve solve_rec solve_rec_rat some somrac sort sparse6_decode sparse6_encode sparse6_export sparse6_import specint spherical spherical_bessel_j spherical_bessel_y spherical_hankel1 spherical_hankel2 spherical_harmonic spherical_to_xyz splice split sposition sprint sqfr sqrt sqrtdenest sremove sremovefirst sreverse ssearch ssort sstatus ssubst ssubstfirst staircase standardize standardize_inverse_trig starplot starplot_description status std std1 std_bernoulli std_beta std_binomial std_chi2 std_continuous_uniform std_discrete_uniform std_exp std_f std_gamma std_general_finite_discrete std_geometric std_gumbel std_hypergeometric std_laplace std_logistic std_lognormal std_negative_binomial std_noncentral_chi2 std_noncentral_student_t std_normal std_pareto std_poisson std_rayleigh std_student_t std_weibull stemplot stirling stirling1 stirling2 strim striml strimr string stringout stringp strong_components struve_h struve_l sublis sublist sublist_indices submatrix subsample subset subsetp subst substinpart subst_parallel substpart substring subvar subvarp sum sumcontract summand_to_rec supcase supcontext symbolp symmdifference symmetricp system take_channel take_inference tan tanh taylor taylorinfo taylorp taylor_simplifier taytorat tcl_output tcontract tellrat tellsimp tellsimpafter tentex tenth test_mean test_means_difference test_normality test_proportion test_proportions_difference test_rank_sum test_sign test_signed_rank test_variance test_variance_ratio tex tex1 tex_display texput %th third throw time timedate timer timer_info tldefint tlimit todd_coxeter toeplitz tokens to_lisp topological_sort to_poly to_poly_solve totaldisrep totalfourier totient tpartpol trace tracematrix trace_options transform_sample translate translate_file transpose treefale tree_reduce treillis treinat triangle triangularize trigexpand trigrat trigreduce trigsimp trunc truncate truncated_cube_graph truncated_dodecahedron_graph truncated_icosahedron_graph truncated_tetrahedron_graph tr_warnings_get tube tutte_graph ueivects uforget ultraspherical underlying_graph undiff union unique uniteigenvectors unitp units unit_step unitvector unorder unsum untellrat untimer untrace uppercasep uricci uriemann uvect vandermonde_matrix var var1 var_bernoulli var_beta var_binomial var_chi2 var_continuous_uniform var_discrete_uniform var_exp var_f var_gamma var_general_finite_discrete var_geometric var_gumbel var_hypergeometric var_laplace var_logistic var_lognormal var_negative_binomial var_noncentral_chi2 var_noncentral_student_t var_normal var_pareto var_poisson var_rayleigh var_student_t var_weibull vector vectorpotential vectorsimp verbify vers vertex_coloring vertex_connectivity vertex_degree vertex_distance vertex_eccentricity vertex_in_degree vertex_out_degree vertices vertices_to_cycle vertices_to_path %w weyl wheel_graph wiener_index wigner_3j wigner_6j wigner_9j with_stdout write_binary_data writebyte write_data writefile wronskian xreduce xthru %y Zeilberger zeroequiv zerofor zeromatrix zeromatrixp zeta zgeev zheev zlange zn_add_table zn_carmichael_lambda zn_characteristic_factors zn_determinant zn_factor_generators zn_invert_by_lu zn_log zn_mult_table absboxchar activecontexts adapt_depth additive adim aform algebraic algepsilon algexact aliases allbut all_dotsimp_denoms allocation allsym alphabetic animation antisymmetric arrays askexp assume_pos assume_pos_pred assumescalar asymbol atomgrad atrig1 axes axis_3d axis_bottom axis_left axis_right axis_top azimuth background background_color backsubst berlefact bernstein_explicit besselexpand beta_args_sum_to_integer beta_expand bftorat bftrunc bindtest border boundaries_array box boxchar breakup %c capping cauchysum cbrange cbtics center cflength cframe_flag cnonmet_flag color color_bar color_bar_tics colorbox columns commutative complex cone context contexts contour contour_levels cosnpiflag ctaypov ctaypt ctayswitch ctayvar ct_coords ctorsion_flag ctrgsimp cube current_let_rule_package cylinder data_file_name debugmode decreasing default_let_rule_package delay dependencies derivabbrev derivsubst detout diagmetric diff dim dimensions dispflag display2d|10 display_format_internal distribute_over doallmxops domain domxexpt domxmxops domxnctimes dontfactor doscmxops doscmxplus dot0nscsimp dot0simp dot1simp dotassoc dotconstrules dotdistrib dotexptsimp dotident dotscrules draw_graph_program draw_realpart edge_color edge_coloring edge_partition edge_type edge_width %edispflag elevation %emode endphi endtheta engineering_format_floats enhanced3d %enumer epsilon_lp erfflag erf_representation errormsg error_size error_syms error_type %e_to_numlog eval even evenfun evflag evfun ev_point expandwrt_denom expintexpand expintrep expon expop exptdispflag exptisolate exptsubst facexpand facsum_combine factlim factorflag factorial_expand factors_only fb feature features file_name file_output_append file_search_demo file_search_lisp file_search_maxima|10 file_search_tests file_search_usage file_type_lisp file_type_maxima|10 fill_color fill_density filled_func fixed_vertices flipflag float2bf font font_size fortindent fortspaces fpprec fpprintprec functions gamma_expand gammalim gdet genindex gensumnum GGFCFMAX GGFINFINITY globalsolve gnuplot_command gnuplot_curve_styles gnuplot_curve_titles gnuplot_default_term_command gnuplot_dumb_term_command gnuplot_file_args gnuplot_file_name gnuplot_out_file gnuplot_pdf_term_command gnuplot_pm3d gnuplot_png_term_command gnuplot_postamble gnuplot_preamble gnuplot_ps_term_command gnuplot_svg_term_command gnuplot_term gnuplot_view_args Gosper_in_Zeilberger gradefs grid grid2d grind halfangles head_angle head_both head_length head_type height hypergeometric_representation %iargs ibase icc1 icc2 icounter idummyx ieqnprint ifb ifc1 ifc2 ifg ifgi ifr iframe_bracket_form ifri igeowedge_flag ikt1 ikt2 imaginary inchar increasing infeval infinity inflag infolists inm inmc1 inmc2 intanalysis integer integervalued integrate_use_rootsof integration_constant integration_constant_counter interpolate_color intfaclim ip_grid ip_grid_in irrational isolate_wrt_times iterations itr julia_parameter %k1 %k2 keepfloat key key_pos kinvariant kt label label_alignment label_orientation labels lassociative lbfgs_ncorrections lbfgs_nfeval_max leftjust legend letrat let_rule_packages lfg lg lhospitallim limsubst linear linear_solver linechar linel|10 linenum line_type linewidth line_width linsolve_params linsolvewarn lispdisp listarith listconstvars listdummyvars lmxchar load_pathname loadprint logabs logarc logcb logconcoeffp logexpand lognegint logsimp logx logx_secondary logy logy_secondary logz lriem m1pbranch macroexpansion macros mainvar manual_demo maperror mapprint matrix_element_add matrix_element_mult matrix_element_transpose maxapplydepth maxapplyheight maxima_tempdir|10 maxima_userdir|10 maxnegex MAX_ORD maxposex maxpsifracdenom maxpsifracnum maxpsinegint maxpsiposint maxtayorder mesh_lines_color method mod_big_prime mode_check_errorp mode_checkp mode_check_warnp mod_test mod_threshold modular_linear_solver modulus multiplicative multiplicities myoptions nary negdistrib negsumdispflag newline newtonepsilon newtonmaxiter nextlayerfactor niceindicespref nm nmc noeval nolabels nonegative_lp noninteger nonscalar noun noundisp nouns np npi nticks ntrig numer numer_pbranch obase odd oddfun opacity opproperties opsubst optimprefix optionset orientation origin orthopoly_returns_intervals outative outchar packagefile palette partswitch pdf_file pfeformat phiresolution %piargs piece pivot_count_sx pivot_max_sx plot_format plot_options plot_realpart png_file pochhammer_max_index points pointsize point_size points_joined point_type poislim poisson poly_coefficient_ring poly_elimination_order polyfactor poly_grobner_algorithm poly_grobner_debug poly_monomial_order poly_primary_elimination_order poly_return_term_list poly_secondary_elimination_order poly_top_reduction_only posfun position powerdisp pred prederror primep_number_of_tests product_use_gamma program programmode promote_float_to_bigfloat prompt proportional_axes props psexpand ps_file radexpand radius radsubstflag rassociative ratalgdenom ratchristof ratdenomdivide rateinstein ratepsilon ratfac rational ratmx ratprint ratriemann ratsimpexpons ratvarswitch ratweights ratweyl ratwtlvl real realonly redraw refcheck resolution restart resultant ric riem rmxchar %rnum_list rombergabs rombergit rombergmin rombergtol rootsconmode rootsepsilon run_viewer same_xy same_xyz savedef savefactors scalar scalarmatrixp scale scale_lp setcheck setcheckbreak setval show_edge_color show_edges show_edge_type show_edge_width show_id show_label showtime show_vertex_color show_vertex_size show_vertex_type show_vertices show_weight simp simplified_output simplify_products simpproduct simpsum sinnpiflag solvedecomposes solveexplicit solvefactors solvenullwarn solveradcan solvetrigwarn space sparse sphere spring_embedding_depth sqrtdispflag stardisp startphi starttheta stats_numer stringdisp structures style sublis_apply_lambda subnumsimp sumexpand sumsplitfact surface surface_hide svg_file symmetric tab taylordepth taylor_logexpand taylor_order_coefficients taylor_truncate_polynomials tensorkill terminal testsuite_files thetaresolution timer_devalue title tlimswitch tr track transcompile transform transform_xy translate_fast_arrays transparent transrun tr_array_as_ref tr_bound_function_applyp tr_file_tty_messagesp tr_float_can_branch_complex tr_function_call_default trigexpandplus trigexpandtimes triginverses trigsign trivial_solutions tr_numer tr_optimize_max_loop tr_semicompile tr_state_vars tr_warn_bad_function_calls tr_warn_fexpr tr_warn_meval tr_warn_mode tr_warn_undeclared tr_warn_undefined_variable tstep ttyoff tube_extremes ufg ug %unitexpand unit_vectors uric uriem use_fast_arrays user_preamble usersetunits values vect_cross verbose vertex_color vertex_coloring vertex_partition vertex_size vertex_type view warnings weyl width windowname windowtitle wired_surface wireframe xaxis xaxis_color xaxis_secondary xaxis_type xaxis_width xlabel xlabel_secondary xlength xrange xrange_secondary xtics xtics_axis xtics_rotate xtics_rotate_secondary xtics_secondary xtics_secondary_axis xu_grid x_voxel xy_file xyplane xy_scale yaxis yaxis_color yaxis_secondary yaxis_type yaxis_width ylabel ylabel_secondary ylength yrange yrange_secondary ytics ytics_axis ytics_rotate ytics_rotate_secondary ytics_secondary ytics_secondary_axis yv_grid y_voxel yx_ratio zaxis zaxis_color zaxis_type zaxis_width zeroa zerob zerobern zeta%pi zlabel zlabel_rotate zlength zmin zn_primroot_limit zn_primroot_pretest",
 symbol:"_ __ %|0 %%|0"},contains:[{className:"comment",begin:"/\\*",end:"\\*/",contains:["self"]},a.QUOTE_STRING_MODE,{className:"number",relevance:0,variants:[{begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Ee][-+]?\\d+\\b"},{begin:"\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Bb][-+]?\\d+\\b",relevance:10},{begin:"\\b(\\.\\d+|\\d+\\.\\d+)\\b"},{begin:"\\b(\\d+|0[0-9A-Za-z]+)\\.?\\b"}]}],illegal:/@/}});b.registerLanguage("mel",function(a){return{keywords:"int float string vector matrix if else switch case default while do for in break continue global proc return about abs addAttr addAttributeEditorNodeHelp addDynamic addNewShelfTab addPP addPanelCategory addPrefixToName advanceToNextDrivenKey affectedNet affects aimConstraint air alias aliasAttr align alignCtx alignCurve alignSurface allViewFit ambientLight angle angleBetween animCone animCurveEditor animDisplay animView annotate appendStringArray applicationName applyAttrPreset applyTake arcLenDimContext arcLengthDimension arclen arrayMapper art3dPaintCtx artAttrCtx artAttrPaintVertexCtx artAttrSkinPaintCtx artAttrTool artBuildPaintMenu artFluidAttrCtx artPuttyCtx artSelectCtx artSetPaintCtx artUserPaintCtx assignCommand assignInputDevice assignViewportFactories attachCurve attachDeviceAttr attachSurface attrColorSliderGrp attrCompatibility attrControlGrp attrEnumOptionMenu attrEnumOptionMenuGrp attrFieldGrp attrFieldSliderGrp attrNavigationControlGrp attrPresetEditWin attributeExists attributeInfo attributeMenu attributeQuery autoKeyframe autoPlace bakeClip bakeFluidShading bakePartialHistory bakeResults bakeSimulation basename basenameEx batchRender bessel bevel bevelPlus binMembership bindSkin blend2 blendShape blendShapeEditor blendShapePanel blendTwoAttr blindDataType boneLattice boundary boxDollyCtx boxZoomCtx bufferCurve buildBookmarkMenu buildKeyframeMenu button buttonManip CBG cacheFile cacheFileCombine cacheFileMerge cacheFileTrack camera cameraView canCreateManip canvas capitalizeString catch catchQuiet ceil changeSubdivComponentDisplayLevel changeSubdivRegion channelBox character characterMap characterOutlineEditor characterize chdir checkBox checkBoxGrp checkDefaultRenderGlobals choice circle circularFillet clamp clear clearCache clip clipEditor clipEditorCurrentTimeCtx clipSchedule clipSchedulerOutliner clipTrimBefore closeCurve closeSurface cluster cmdFileOutput cmdScrollFieldExecuter cmdScrollFieldReporter cmdShell coarsenSubdivSelectionList collision color colorAtPoint colorEditor colorIndex colorIndexSliderGrp colorSliderButtonGrp colorSliderGrp columnLayout commandEcho commandLine commandPort compactHairSystem componentEditor compositingInterop computePolysetVolume condition cone confirmDialog connectAttr connectControl connectDynamic connectJoint connectionInfo constrain constrainValue constructionHistory container containsMultibyte contextInfo control convertFromOldLayers convertIffToPsd convertLightmap convertSolidTx convertTessellation convertUnit copyArray copyFlexor copyKey copySkinWeights cos cpButton cpCache cpClothSet cpCollision cpConstraint cpConvClothToMesh cpForces cpGetSolverAttr cpPanel cpProperty cpRigidCollisionFilter cpSeam cpSetEdit cpSetSolverAttr cpSolver cpSolverTypes cpTool cpUpdateClothUVs createDisplayLayer createDrawCtx createEditor createLayeredPsdFile createMotionField createNewShelf createNode createRenderLayer createSubdivRegion cross crossProduct ctxAbort ctxCompletion ctxEditMode ctxTraverse currentCtx currentTime currentTimeCtx currentUnit curve curveAddPtCtx curveCVCtx curveEPCtx curveEditorCtx curveIntersect curveMoveEPCtx curveOnSurface curveSketchCtx cutKey cycleCheck cylinder dagPose date defaultLightListCheckBox defaultNavigation defineDataServer defineVirtualDevice deformer deg_to_rad delete deleteAttr deleteShadingGroupsAndMaterials deleteShelfTab deleteUI deleteUnusedBrushes delrandstr detachCurve detachDeviceAttr detachSurface deviceEditor devicePanel dgInfo dgdirty dgeval dgtimer dimWhen directKeyCtx directionalLight dirmap dirname disable disconnectAttr disconnectJoint diskCache displacementToPoly displayAffected displayColor displayCull displayLevelOfDetail displayPref displayRGBColor displaySmoothness displayStats displayString displaySurface distanceDimContext distanceDimension doBlur dolly dollyCtx dopeSheetEditor dot dotProduct doubleProfileBirailSurface drag dragAttrContext draggerContext dropoffLocator duplicate duplicateCurve duplicateSurface dynCache dynControl dynExport dynExpression dynGlobals dynPaintEditor dynParticleCtx dynPref dynRelEdPanel dynRelEditor dynamicLoad editAttrLimits editDisplayLayerGlobals editDisplayLayerMembers editRenderLayerAdjustment editRenderLayerGlobals editRenderLayerMembers editor editorTemplate effector emit emitter enableDevice encodeString endString endsWith env equivalent equivalentTol erf error eval evalDeferred evalEcho event exactWorldBoundingBox exclusiveLightCheckBox exec executeForEachObject exists exp expression expressionEditorListen extendCurve extendSurface extrude fcheck fclose feof fflush fgetline fgetword file fileBrowserDialog fileDialog fileExtension fileInfo filetest filletCurve filter filterCurve filterExpand filterStudioImport findAllIntersections findAnimCurves findKeyframe findMenuItem findRelatedSkinCluster finder firstParentOf fitBspline flexor floatEq floatField floatFieldGrp floatScrollBar floatSlider floatSlider2 floatSliderButtonGrp floatSliderGrp floor flow fluidCacheInfo fluidEmitter fluidVoxelInfo flushUndo fmod fontDialog fopen formLayout format fprint frameLayout fread freeFormFillet frewind fromNativePath fwrite gamma gauss geometryConstraint getApplicationVersionAsFloat getAttr getClassification getDefaultBrush getFileList getFluidAttr getInputDeviceRange getMayaPanelTypes getModifiers getPanel getParticleAttr getPluginResource getenv getpid glRender glRenderEditor globalStitch gmatch goal gotoBindPose grabColor gradientControl gradientControlNoAttr graphDollyCtx graphSelectContext graphTrackCtx gravity grid gridLayout group groupObjectsByName HfAddAttractorToAS HfAssignAS HfBuildEqualMap HfBuildFurFiles HfBuildFurImages HfCancelAFR HfConnectASToHF HfCreateAttractor HfDeleteAS HfEditAS HfPerformCreateAS HfRemoveAttractorFromAS HfSelectAttached HfSelectAttractors HfUnAssignAS hardenPointCurve hardware hardwareRenderPanel headsUpDisplay headsUpMessage help helpLine hermite hide hilite hitTest hotBox hotkey hotkeyCheck hsv_to_rgb hudButton hudSlider hudSliderButton hwReflectionMap hwRender hwRenderLoad hyperGraph hyperPanel hyperShade hypot iconTextButton iconTextCheckBox iconTextRadioButton iconTextRadioCollection iconTextScrollList iconTextStaticLabel ikHandle ikHandleCtx ikHandleDisplayScale ikSolver ikSplineHandleCtx ikSystem ikSystemInfo ikfkDisplayMethod illustratorCurves image imfPlugins inheritTransform insertJoint insertJointCtx insertKeyCtx insertKnotCurve insertKnotSurface instance instanceable instancer intField intFieldGrp intScrollBar intSlider intSliderGrp interToUI internalVar intersect iprEngine isAnimCurve isConnected isDirty isParentOf isSameObject isTrue isValidObjectName isValidString isValidUiName isolateSelect itemFilter itemFilterAttr itemFilterRender itemFilterType joint jointCluster jointCtx jointDisplayScale jointLattice keyTangent keyframe keyframeOutliner keyframeRegionCurrentTimeCtx keyframeRegionDirectKeyCtx keyframeRegionDollyCtx keyframeRegionInsertKeyCtx keyframeRegionMoveKeyCtx keyframeRegionScaleKeyCtx keyframeRegionSelectKeyCtx keyframeRegionSetKeyCtx keyframeRegionTrackCtx keyframeStats lassoContext lattice latticeDeformKeyCtx launch launchImageEditor layerButton layeredShaderPort layeredTexturePort layout layoutDialog lightList lightListEditor lightListPanel lightlink lineIntersection linearPrecision linstep listAnimatable listAttr listCameras listConnections listDeviceAttachments listHistory listInputDeviceAxes listInputDeviceButtons listInputDevices listMenuAnnotation listNodeTypes listPanelCategories listRelatives listSets listTransforms listUnselected listerEditor loadFluid loadNewShelf loadPlugin loadPluginLanguageResources loadPrefObjects localizedPanelLabel lockNode loft log longNameOf lookThru ls lsThroughFilter lsType lsUI Mayatomr mag makeIdentity makeLive makePaintable makeRoll makeSingleSurface makeTubeOn makebot manipMoveContext manipMoveLimitsCtx manipOptions manipRotateContext manipRotateLimitsCtx manipScaleContext manipScaleLimitsCtx marker match max memory menu menuBarLayout menuEditor menuItem menuItemToShelf menuSet menuSetPref messageLine min minimizeApp mirrorJoint modelCurrentTimeCtx modelEditor modelPanel mouse movIn movOut move moveIKtoFK moveKeyCtx moveVertexAlongDirection multiProfileBirailSurface mute nParticle nameCommand nameField namespace namespaceInfo newPanelItems newton nodeCast nodeIconButton nodeOutliner nodePreset nodeType noise nonLinear normalConstraint normalize nurbsBoolean nurbsCopyUVSet nurbsCube nurbsEditUV nurbsPlane nurbsSelect nurbsSquare nurbsToPoly nurbsToPolygonsPref nurbsToSubdiv nurbsToSubdivPref nurbsUVSet nurbsViewDirectionVector objExists objectCenter objectLayer objectType objectTypeUI obsoleteProc oceanNurbsPreviewPlane offsetCurve offsetCurveOnSurface offsetSurface openGLExtension openMayaPref optionMenu optionMenuGrp optionVar orbit orbitCtx orientConstraint outlinerEditor outlinerPanel overrideModifier paintEffectsDisplay pairBlend palettePort paneLayout panel panelConfiguration panelHistory paramDimContext paramDimension paramLocator parent parentConstraint particle particleExists particleInstancer particleRenderInfo partition pasteKey pathAnimation pause pclose percent performanceOptions pfxstrokes pickWalk picture pixelMove planarSrf plane play playbackOptions playblast plugAttr plugNode pluginInfo pluginResourceUtil pointConstraint pointCurveConstraint pointLight pointMatrixMult pointOnCurve pointOnSurface pointPosition poleVectorConstraint polyAppend polyAppendFacetCtx polyAppendVertex polyAutoProjection polyAverageNormal polyAverageVertex polyBevel polyBlendColor polyBlindData polyBoolOp polyBridgeEdge polyCacheMonitor polyCheck polyChipOff polyClipboard polyCloseBorder polyCollapseEdge polyCollapseFacet polyColorBlindData polyColorDel polyColorPerVertex polyColorSet polyCompare polyCone polyCopyUV polyCrease polyCreaseCtx polyCreateFacet polyCreateFacetCtx polyCube polyCut polyCutCtx polyCylinder polyCylindricalProjection polyDelEdge polyDelFacet polyDelVertex polyDuplicateAndConnect polyDuplicateEdge polyEditUV polyEditUVShell polyEvaluate polyExtrudeEdge polyExtrudeFacet polyExtrudeVertex polyFlipEdge polyFlipUV polyForceUV polyGeoSampler polyHelix polyInfo polyInstallAction polyLayoutUV polyListComponentConversion polyMapCut polyMapDel polyMapSew polyMapSewMove polyMergeEdge polyMergeEdgeCtx polyMergeFacet polyMergeFacetCtx polyMergeUV polyMergeVertex polyMirrorFace polyMoveEdge polyMoveFacet polyMoveFacetUV polyMoveUV polyMoveVertex polyNormal polyNormalPerVertex polyNormalizeUV polyOptUvs polyOptions polyOutput polyPipe polyPlanarProjection polyPlane polyPlatonicSolid polyPoke polyPrimitive polyPrism polyProjection polyPyramid polyQuad polyQueryBlindData polyReduce polySelect polySelectConstraint polySelectConstraintMonitor polySelectCtx polySelectEditCtx polySeparate polySetToFaceNormal polySewEdge polyShortestPathCtx polySmooth polySoftEdge polySphere polySphericalProjection polySplit polySplitCtx polySplitEdge polySplitRing polySplitVertex polyStraightenUVBorder polySubdivideEdge polySubdivideFacet polyToSubdiv polyTorus polyTransfer polyTriangulate polyUVSet polyUnite polyWedgeFace popen popupMenu pose pow preloadRefEd print progressBar progressWindow projFileViewer projectCurve projectTangent projectionContext projectionManip promptDialog propModCtx propMove psdChannelOutliner psdEditTextureFile psdExport psdTextureFile putenv pwd python querySubdiv quit rad_to_deg radial radioButton radioButtonGrp radioCollection radioMenuItemCollection rampColorPort rand randomizeFollicles randstate rangeControl readTake rebuildCurve rebuildSurface recordAttr recordDevice redo reference referenceEdit referenceQuery refineSubdivSelectionList refresh refreshAE registerPluginResource rehash reloadImage removeJoint removeMultiInstance removePanelCategory rename renameAttr renameSelectionList renameUI render renderGlobalsNode renderInfo renderLayerButton renderLayerParent renderLayerPostProcess renderLayerUnparent renderManip renderPartition renderQualityNode renderSettings renderThumbnailUpdate renderWindowEditor renderWindowSelectContext renderer reorder reorderDeformers requires reroot resampleFluid resetAE resetPfxToPolyCamera resetTool resolutionNode retarget reverseCurve reverseSurface revolve rgb_to_hsv rigidBody rigidSolver roll rollCtx rootOf rot rotate rotationInterpolation roundConstantRadius rowColumnLayout rowLayout runTimeCommand runup sampleImage saveAllShelves saveAttrPreset saveFluid saveImage saveInitialState saveMenu savePrefObjects savePrefs saveShelf saveToolSettings scale scaleBrushBrightness scaleComponents scaleConstraint scaleKey scaleKeyCtx sceneEditor sceneUIReplacement scmh scriptCtx scriptEditorInfo scriptJob scriptNode scriptTable scriptToShelf scriptedPanel scriptedPanelType scrollField scrollLayout sculpt searchPathArray seed selLoadSettings select selectContext selectCurveCV selectKey selectKeyCtx selectKeyframeRegionCtx selectMode selectPref selectPriority selectType selectedNodes selectionConnection separator setAttr setAttrEnumResource setAttrMapping setAttrNiceNameResource setConstraintRestPosition setDefaultShadingGroup setDrivenKeyframe setDynamic setEditCtx setEditor setFluidAttr setFocus setInfinity setInputDeviceMapping setKeyCtx setKeyPath setKeyframe setKeyframeBlendshapeTargetWts setMenuMode setNodeNiceNameResource setNodeTypeFlag setParent setParticleAttr setPfxToPolyCamera setPluginResource setProject setStampDensity setStartupMessage setState setToolTo setUITemplate setXformManip sets shadingConnection shadingGeometryRelCtx shadingLightRelCtx shadingNetworkCompare shadingNode shapeCompare shelfButton shelfLayout shelfTabLayout shellField shortNameOf showHelp showHidden showManipCtx showSelectionInTitle showShadingGroupAttrEditor showWindow sign simplify sin singleProfileBirailSurface size sizeBytes skinCluster skinPercent smoothCurve smoothTangentSurface smoothstep snap2to2 snapKey snapMode snapTogetherCtx snapshot soft softMod softModCtx sort sound soundControl source spaceLocator sphere sphrand spotLight spotLightPreviewPort spreadSheetEditor spring sqrt squareSurface srtContext stackTrace startString startsWith stitchAndExplodeShell stitchSurface stitchSurfacePoints strcmp stringArrayCatenate stringArrayContains stringArrayCount stringArrayInsertAtIndex stringArrayIntersector stringArrayRemove stringArrayRemoveAtIndex stringArrayRemoveDuplicates stringArrayRemoveExact stringArrayToString stringToStringArray strip stripPrefixFromName stroke subdAutoProjection subdCleanTopology subdCollapse subdDuplicateAndConnect subdEditUV subdListComponentConversion subdMapCut subdMapSewMove subdMatchTopology subdMirror subdToBlind subdToPoly subdTransferUVsToCache subdiv subdivCrease subdivDisplaySmoothness substitute substituteAllString substituteGeometry substring surface surfaceSampler surfaceShaderList swatchDisplayPort switchTable symbolButton symbolCheckBox sysFile system tabLayout tan tangentConstraint texLatticeDeformContext texManipContext texMoveContext texMoveUVShellContext texRotateContext texScaleContext texSelectContext texSelectShortestPathCtx texSmudgeUVContext texWinToolCtx text textCurves textField textFieldButtonGrp textFieldGrp textManip textScrollList textToShelf textureDisplacePlane textureHairColor texturePlacementContext textureWindow threadCount threePointArcCtx timeControl timePort timerX toNativePath toggle toggleAxis toggleWindowVisibility tokenize tokenizeList tolerance tolower toolButton toolCollection toolDropped toolHasOptions toolPropertyWindow torus toupper trace track trackCtx transferAttributes transformCompare transformLimits translator trim trunc truncateFluidCache truncateHairCache tumble tumbleCtx turbulence twoPointArcCtx uiRes uiTemplate unassignInputDevice undo undoInfo ungroup uniform unit unloadPlugin untangleUV untitledFileName untrim upAxis updateAE userCtx uvLink uvSnapshot validateShelfName vectorize view2dToolCtx viewCamera viewClipPlane viewFit viewHeadOn viewLookAt viewManip viewPlace viewSet visor volumeAxis vortex waitCursor warning webBrowser webBrowserPrefs whatIs window windowPref wire wireContext workspace wrinkle wrinkleContext writeTake xbmLangPathList xform",
 illegal:"</",contains:[a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"[\\$\\%\\@](\\^\\w\\b|#\\w+|[^\\s\\w{]|{\\w+}|\\w+)"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}});b.registerLanguage("mercury",function(a){var b=a.COMMENT("%","$"),d=a.inherit(a.APOS_STRING_MODE,{relevance:0}),e=a.inherit(a.QUOTE_STRING_MODE,{relevance:0});e.contains.push({className:"subst",begin:"\\\\[abfnrtv]\\|\\\\x[0-9a-fA-F]*\\\\\\|%[-+# *.0-9]*[dioxXucsfeEgGp]",
 relevance:0});return{aliases:["m","moo"],keywords:{keyword:"module use_module import_module include_module end_module initialise mutable initialize finalize finalise interface implementation pred mode func type inst solver any_pred any_func is semidet det nondet multi erroneous failure cc_nondet cc_multi typeclass instance where pragma promise external trace atomic or_else require_complete_switch require_det require_semidet require_multi require_nondet require_cc_multi require_cc_nondet require_erroneous require_failure",
@@ -292,7 +310,7 @@
 literal:"shared guarded stdin stdout stderr result true false",built_in:"int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float float32 float64 bool char string cstring pointer expr stmt void auto any range array openarray varargs seq set clong culong cchar cschar cshort cint csize clonglong cfloat cdouble clongdouble cuchar cushort cuint culonglong cstringarray semistatic"},contains:[{className:"meta",begin:/{\./,end:/\.}/,relevance:10},{className:"string",begin:/[a-zA-Z]\w*"/,end:/"/,
 contains:[{begin:/""/}]},{className:"string",begin:/([a-zA-Z]\w*)?"""/,end:/"""/},a.QUOTE_STRING_MODE,{className:"type",begin:/\b[A-Z]\w+\b/,relevance:0},{className:"number",relevance:0,variants:[{begin:/\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/},{begin:/\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/}]},a.HASH_COMMENT_MODE]}});b.registerLanguage("nix",function(a){var b={keyword:"rec with let in inherit assert if else then",
 literal:"true false or and null",built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"},d={className:"subst",begin:/\$\{/,end:/}/,keywords:b};a=[a.NUMBER_MODE,a.HASH_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",contains:[d],variants:[{begin:"''",end:"''"},{begin:'"',end:'"'}]},{begin:/[a-zA-Z0-9-_]+(\s*=)/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/\S+/}]}];d.contains=a;return{aliases:["nixos"],keywords:b,contains:a}});b.registerLanguage("nsis",
-function(a){var b={className:"variable",begin:/\$+{[\w\.:-]+}/},d={className:"variable",begin:/\$+\w+/,illegal:/\(\){}/},e={className:"variable",begin:/\$+\([\w\^\.:-]+\)/},f={className:"string",variants:[{begin:'"',end:'"'},{begin:"'",end:"'"},{begin:"`",end:"`"}],illegal:/\n/,contains:[{begin:/\$(\\[nrt]|\$)/},{className:"variable",begin:/\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)/},
+function(a){var b={className:"variable",begin:/\$+{[\w\.:-]+}/},d={className:"variable",begin:/\$+\w+/,illegal:/\(\){}/},e={className:"variable",begin:/\$+\([\w\^\.:-]+\)/},f={className:"string",variants:[{begin:'"',end:'"'},{begin:"'",end:"'"},{begin:"`",end:"`"}],illegal:/\n/,contains:[{className:"meta",begin:/\$(\\[nrt]|\$)/},{className:"variable",begin:/\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)/},
 b,d,e]};return{case_insensitive:!1,keywords:{keyword:"Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ChangeUI CheckBitmap ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exch Exec ExecShell ExecShellWait ExecWait ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileReadUTF16LE FileReadWord FileSeek FileWrite FileWriteByte FileWriteUTF16LE FileWriteWord FindClose FindFirst FindNext FindWindow FlushINI FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText Int64Cmp Int64CmpU Int64Fmt IntCmp IntCmpU IntFmt IntOp IntPtrCmp IntPtrCmpU IntPtrOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LockWindow LogSet LogText ManifestDPIAware ManifestSupportedOS MessageBox MiscButtonText Name Nop OutFile Page PageCallbacks PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename RequestExecutionLevel ReserveFile Return RMDir SearchPath SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionGroupEnd SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetRebootFlag SetRegView SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCmpS StrCpy StrLen SubCaption Unicode UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIFileVersion VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegMultiStr WriteRegNone WriteRegStr WriteUninstaller XPStyle",
 literal:"admin all auto both bottom bzip2 colored components current custom directory false force hide highest ifdiff ifnewer instfiles lastused leave left license listonly lzma nevershow none normal notset off on open print right show silent silentlog smooth textonly top true try un.components un.custom un.directory un.instfiles un.license uninstConfirm user Win10 Win7 Win8 WinVista zlib"},contains:[a.HASH_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.COMMENT(";","$",{relevance:0}),{className:"function",
 beginKeywords:"Function PageEx Section SectionGroup",end:"$"},f,{className:"keyword",begin:/!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|else|endif|error|execute|finalize|getdllversion|gettlbversion|if|ifdef|ifmacrodef|ifmacrondef|ifndef|include|insertmacro|macro|macroend|makensis|packhdr|searchparse|searchreplace|system|tempfile|undef|verbose|warning)/},b,d,e,{className:"params",begin:"(ARCHIVE|FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_OFFLINE|FILE_ATTRIBUTE_READONLY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_TEMPORARY|HKCR|HKCU|HKDD|HKEY_CLASSES_ROOT|HKEY_CURRENT_CONFIG|HKEY_CURRENT_USER|HKEY_DYN_DATA|HKEY_LOCAL_MACHINE|HKEY_PERFORMANCE_DATA|HKEY_USERS|HKLM|HKPD|HKU|IDABORT|IDCANCEL|IDIGNORE|IDNO|IDOK|IDRETRY|IDYES|MB_ABORTRETRYIGNORE|MB_DEFBUTTON1|MB_DEFBUTTON2|MB_DEFBUTTON3|MB_DEFBUTTON4|MB_ICONEXCLAMATION|MB_ICONINFORMATION|MB_ICONQUESTION|MB_ICONSTOP|MB_OK|MB_OKCANCEL|MB_RETRYCANCEL|MB_RIGHT|MB_RTLREADING|MB_SETFOREGROUND|MB_TOPMOST|MB_USERICON|MB_YESNO|NORMAL|OFFLINE|READONLY|SHCTX|SHELL_CONTEXT|SYSTEM|TEMPORARY)"},
@@ -308,35 +326,57 @@
 illegal:'("|\\$[G-Zg-z]|\\/\\*|</|=>|->)',contains:[b,d,a.C_LINE_COMMENT_MODE,e,f,a.NUMBER_MODE,g,{className:"class",begin:"=\\bclass\\b",end:"end;",keywords:"abstract add and array as asc aspect assembly async begin break block by case class concat const copy constructor continue create default delegate desc distinct div do downto dynamic each else empty end ensure enum equals event except exit extension external false final finalize finalizer finally flags for forward from function future global group has if implementation implements implies in index inherited inline interface into invariants is iterator join locked locking loop matching method mod module namespace nested new nil not notify nullable of old on operator or order out override parallel params partial pinned private procedure property protected public queryable raise read readonly record reintroduce remove repeat require result reverse sealed select self sequence set shl shr skip static step soft take then to true try tuple type union unit unsafe until uses using var virtual raises volatile where while with write xor yield await mapped deprecated stdcall cdecl pascal register safecall overload library platform reference packed strict published autoreleasepool selector strong weak unretained",
 contains:[e,f,b,d,a.C_LINE_COMMENT_MODE,g]}]}});b.registerLanguage("parser3",function(a){var b=a.COMMENT("{","}",{contains:["self"]});return{subLanguage:"xml",relevance:0,contains:[a.COMMENT("^#","$"),a.COMMENT("\\^rem{","}",{relevance:10,contains:[b]}),{className:"meta",begin:"^@(?:BASE|USE|CLASS|OPTIONS)$",relevance:10},{className:"title",begin:"@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$"},{className:"variable",begin:"\\$\\{?[\\w\\-\\.\\:]+\\}?"},{className:"keyword",begin:"\\^[\\w\\-\\.\\:]+"},
 {className:"number",begin:"\\^#[0-9a-fA-F]+"},a.C_NUMBER_MODE]}});b.registerLanguage("pf",function(a){return{aliases:["pf.conf"],lexemes:/[a-z0-9_<>-]+/,keywords:{built_in:"block match pass load anchor|5 antispoof|10 set table",keyword:"in out log quick on rdomain inet inet6 proto from port os to routeallow-opts divert-packet divert-reply divert-to flags group icmp-typeicmp6-type label once probability recieved-on rtable prio queuetos tag tagged user keep fragment for os dropaf-to|10 binat-to|10 nat-to|10 rdr-to|10 bitmask least-stats random round-robinsource-hash static-portdup-to reply-to route-toparent bandwidth default min max qlimitblock-policy debug fingerprints hostid limit loginterface optimizationreassemble ruleset-optimization basic none profile skip state-defaultsstate-policy timeoutconst counters persistno modulate synproxy state|5 floating if-bound no-sync pflow|10 sloppysource-track global rule max-src-nodes max-src-states max-src-connmax-src-conn-rate overload flushscrub|5 max-mss min-ttl no-df|10 random-id",
-literal:"all any no-route self urpf-failed egress|5 unknown"},contains:[a.HASH_COMMENT_MODE,a.NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"variable",begin:/\$[\w\d#@][\w\d_]*/},{className:"variable",begin:/<(?!\/)/,end:/>/}]}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},d={className:"meta",begin:/<\?(php)?|\?>/},e={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,
-{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},f={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["php3","php4","php5","php6"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",
+literal:"all any no-route self urpf-failed egress|5 unknown"},contains:[a.HASH_COMMENT_MODE,a.NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"variable",begin:/\$[\w\d#@][\w\d_]*/},{className:"variable",begin:/<(?!\/)/,end:/>/}]}});b.registerLanguage("pgsql",function(a){var b=a.COMMENT("--","$");"HSTORE|10 LO LTREE|10 ";var d="BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10".split(" ").map(function(a){return a.split("|")[0]}).join("|"),
+e="ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP PERCENTILE_CONT PERCENTILE_DISC ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE NUM_NONNULLS NUM_NULLS ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT TRUNC WIDTH_BUCKET RANDOM SETSEED ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAPLEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR TO_ASCII TO_HEX TRANSLATE OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 TIMEOFDAY TRANSACTION_TIMESTAMP|10 ENUM_FIRST ENUM_LAST ENUM_RANGE AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILYINET_MERGE MACADDR8_SET7BIT ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA CURSOR_TO_XML CURSOR_TO_XMLSCHEMA SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA XMLATTRIBUTES TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY CURRVAL LASTVAL NEXTVAL SETVAL COALESCE NULLIF GREATEST LEAST ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY STRING_TO_ARRAY UNNEST ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE GENERATE_SERIES GENERATE_SUBSCRIPTS CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE GIN_CLEAN_PENDING_LIST SUPPRESS_REDUNDANT_UPDATES_TRIGGER LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE GROUPING CAST".split(" ").map(function(a){return a.split("|")[0]}).join("|");
+return{aliases:["postgres","postgresql"],case_insensitive:!0,keywords:{keyword:"ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION INDEX PROCEDURE ASSERTION ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS DEFERRABLE RANGE DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED OF NOTHING NONE EXCLUDE ATTRIBUTE USAGE ROUTINES TRUE FALSE NAN INFINITY ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT OPEN SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS ",
+built_in:"CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 SQLSTATE SQLERRM|10 SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED INDEX_CORRUPTED "},
+illegal:/:==|\W\s*\(\*|(^|\s)\$[a-z]|{{|[a-z]:\s*$|\.\.\.|TO:|DO:/,contains:[{className:"keyword",variants:[{begin:/\bTEXT\s*SEARCH\b/},{begin:/\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/},{begin:/\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/},{begin:/\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/},{begin:/\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/},{begin:/\bNULLS\s+(FIRST|LAST)\b/},{begin:/\bEVENT\s+TRIGGER\b/},{begin:/\b(MAPPING|OR)\s+REPLACE\b/},{begin:/\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/},{begin:/\b(SHARE|EXCLUSIVE)\s+MODE\b/},
+{begin:/\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/},{begin:/\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/},{begin:/\bPRESERVE\s+ROWS\b/},{begin:/\bDISCARD\s+PLANS\b/},{begin:/\bREFERENCING\s+(OLD|NEW)\b/},{begin:/\bSKIP\s+LOCKED\b/},{begin:/\bGROUPING\s+SETS\b/},{begin:/\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/},{begin:/\b(WITH|WITHOUT)\s+HOLD\b/},{begin:/\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/},{begin:/\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/},{begin:/\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/},
+{begin:/\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/},{begin:/\bIS\s+(NOT\s+)?UNKNOWN\b/},{begin:/\bSECURITY\s+LABEL\b/},{begin:/\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/},{begin:/\bWITH\s+(NO\s+)?DATA\b/},{begin:/\b(FOREIGN|SET)\s+DATA\b/},{begin:/\bSET\s+(CATALOG|CONSTRAINTS)\b/},{begin:/\b(WITH|FOR)\s+ORDINALITY\b/},{begin:/\bIS\s+(NOT\s+)?DOCUMENT\b/},{begin:/\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/},{begin:/\b(STRIP|PRESERVE)\s+WHITESPACE\b/},{begin:/\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/},{begin:/\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/},
+{begin:/\bAT\s+TIME\s+ZONE\b/},{begin:/\bGRANTED\s+BY\b/},{begin:/\bRETURN\s+(QUERY|NEXT)\b/},{begin:/\b(ATTACH|DETACH)\s+PARTITION\b/},{begin:/\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/},{begin:/\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/},{begin:/\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/}]},{begin:/\b(FORMAT|FAMILY|VERSION)\s*\(/},{begin:/\bINCLUDE\s*\(/,keywords:"INCLUDE"},{begin:/\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/},
+{begin:/\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/},{begin:/\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/,relevance:10},{begin:/\bEXTRACT\s*\(/,end:/\bFROM\b/,returnEnd:!0,keywords:{type:"CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR TIMEZONE_MINUTE WEEK YEAR"}},
+{begin:/\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/,keywords:{keyword:"NAME"}},{begin:/\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/,keywords:{keyword:"DOCUMENT CONTENT"}},{beginKeywords:"CACHE INCREMENT MAXVALUE MINVALUE",end:a.C_NUMBER_RE,returnEnd:!0,keywords:"BY CACHE INCREMENT MAXVALUE MINVALUE"},{className:"type",begin:/\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/},{className:"type",begin:/\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/},{begin:/\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/,
+keywords:{keyword:"RETURNS",type:"LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER"}},{begin:"\\b("+e+")\\s*\\("},{begin:"\\.("+d+")\\b"},{begin:"\\b("+d+")\\s+PATH\\b",keywords:{keyword:"PATH",type:"BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ".replace("PATH ",
+"")}},{className:"type",begin:"\\b("+d+")\\b"},{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:"(e|E|u&|U&)'",end:"'",contains:[{begin:"\\\\."}],relevance:10},{begin:"\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",endSameAsBegin:!0,contains:[{subLanguage:"pgsql perl python tcl r lua java php ruby bash scheme xml json".split(" "),endsWithParent:!0}]},{begin:'"',end:'"',contains:[{begin:'""'}]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b,{className:"meta",variants:[{begin:"%(ROW)?TYPE",
+relevance:10},{begin:"\\$\\d+"},{begin:"^#\\w",end:"$"}]},{className:"symbol",begin:"<<\\s*[a-zA-Z_][a-zA-Z_0-9$]*\\s*>>",relevance:10}]}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},d={className:"meta",begin:/<\?(php)?|\?>/},e={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},f={variants:[a.BINARY_NUMBER_MODE,
+a.C_NUMBER_MODE]};return{aliases:"php php3 php4 php5 php6 php7".split(" "),case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",
 contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[d]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},d,{className:"keyword",begin:/\$this\b/},b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},
 {className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:["self",b,a.C_BLOCK_COMMENT_MODE,e,f]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},
-{begin:"=>"},e,f]}});b.registerLanguage("pony",function(a){var b={className:"type",begin:"\\b_?[A-Z][\\w]*",relevance:0},d={begin:a.IDENT_RE+"'",relevance:0};return{keywords:{keyword:"actor addressof and as be break class compile_error compile_intrinsicconsume continue delegate digestof do else elseif embed end errorfor fun if ifdef in interface is isnt lambda let match new not objector primitive recover repeat return struct then trait try type until use var where while with xor",meta:"iso val tag trn box ref",
-literal:"this false true"},contains:[{className:"class",beginKeywords:"class actor",end:"$",contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE]},{className:"function",beginKeywords:"new fun",end:"=>",contains:[a.TITLE_MODE,{begin:/\(/,end:/\)/,contains:[b,d,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},{begin:/:/,endsWithParent:!0,contains:[b]},a.C_LINE_COMMENT_MODE]},b,{className:"string",begin:'"""',end:'"""',relevance:10},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{className:"string",
-begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE],relevance:0},d,a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}});b.registerLanguage("powershell",function(a){var b={begin:"`[\\s\\S]",relevance:0},d={className:"variable",variants:[{begin:/\$[\w\d][\w\d_:]*/}]},e={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[b,d,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},f=a.inherit(a.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],
-contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]});return{aliases:["ps"],lexemes:/-?[A-z\.\-]+/,case_insensitive:!0,keywords:{keyword:"if else foreach return function do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch",built_in:"Add-Computer Add-Content Add-History Add-JobTrigger Add-Member Add-PSSnapin Add-Type Checkpoint-Computer Clear-Content Clear-EventLog Clear-History Clear-Host Clear-Item Clear-ItemProperty Clear-Variable Compare-Object Complete-Transaction Connect-PSSession Connect-WSMan Convert-Path ConvertFrom-Csv ConvertFrom-Json ConvertFrom-SecureString ConvertFrom-StringData ConvertTo-Csv ConvertTo-Html ConvertTo-Json ConvertTo-SecureString ConvertTo-Xml Copy-Item Copy-ItemProperty Debug-Process Disable-ComputerRestore Disable-JobTrigger Disable-PSBreakpoint Disable-PSRemoting Disable-PSSessionConfiguration Disable-WSManCredSSP Disconnect-PSSession Disconnect-WSMan Disable-ScheduledJob Enable-ComputerRestore Enable-JobTrigger Enable-PSBreakpoint Enable-PSRemoting Enable-PSSessionConfiguration Enable-ScheduledJob Enable-WSManCredSSP Enter-PSSession Exit-PSSession Export-Alias Export-Clixml Export-Console Export-Counter Export-Csv Export-FormatData Export-ModuleMember Export-PSSession ForEach-Object Format-Custom Format-List Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command Get-ComputerRestorePoint Get-Content Get-ControlPanelItem Get-Counter Get-Credential Get-Culture Get-Date Get-Event Get-EventLog Get-EventSubscriber Get-ExecutionPolicy Get-FormatData Get-Host Get-HotFix Get-Help Get-History Get-IseSnippet Get-Item Get-ItemProperty Get-Job Get-JobTrigger Get-Location Get-Member Get-Module Get-PfxCertificate Get-Process Get-PSBreakpoint Get-PSCallStack Get-PSDrive Get-PSProvider Get-PSSession Get-PSSessionConfiguration Get-PSSnapin Get-Random Get-ScheduledJob Get-ScheduledJobOption Get-Service Get-TraceSource Get-Transaction Get-TypeData Get-UICulture Get-Unique Get-Variable Get-Verb Get-WinEvent Get-WmiObject Get-WSManCredSSP Get-WSManInstance Group-Object Import-Alias Import-Clixml Import-Counter Import-Csv Import-IseSnippet Import-LocalizedData Import-PSSession Import-Module Invoke-AsWorkflow Invoke-Command Invoke-Expression Invoke-History Invoke-Item Invoke-RestMethod Invoke-WebRequest Invoke-WmiMethod Invoke-WSManAction Join-Path Limit-EventLog Measure-Command Measure-Object Move-Item Move-ItemProperty New-Alias New-Event New-EventLog New-IseSnippet New-Item New-ItemProperty New-JobTrigger New-Object New-Module New-ModuleManifest New-PSDrive New-PSSession New-PSSessionConfigurationFile New-PSSessionOption New-PSTransportOption New-PSWorkflowExecutionOption New-PSWorkflowSession New-ScheduledJobOption New-Service New-TimeSpan New-Variable New-WebServiceProxy New-WinEvent New-WSManInstance New-WSManSessionOption Out-Default Out-File Out-GridView Out-Host Out-Null Out-Printer Out-String Pop-Location Push-Location Read-Host Receive-Job Register-EngineEvent Register-ObjectEvent Register-PSSessionConfiguration Register-ScheduledJob Register-WmiEvent Remove-Computer Remove-Event Remove-EventLog Remove-Item Remove-ItemProperty Remove-Job Remove-JobTrigger Remove-Module Remove-PSBreakpoint Remove-PSDrive Remove-PSSession Remove-PSSnapin Remove-TypeData Remove-Variable Remove-WmiObject Remove-WSManInstance Rename-Computer Rename-Item Rename-ItemProperty Reset-ComputerMachinePassword Resolve-Path Restart-Computer Restart-Service Restore-Computer Resume-Job Resume-Service Save-Help Select-Object Select-String Select-Xml Send-MailMessage Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-JobTrigger Set-Location Set-PSBreakpoint Set-PSDebug Set-PSSessionConfiguration Set-ScheduledJob Set-ScheduledJobOption Set-Service Set-StrictMode Set-TraceSource Set-Variable Set-WmiInstance Set-WSManInstance Set-WSManQuickConfig Show-Command Show-ControlPanelItem Show-EventLog Sort-Object Split-Path Start-Job Start-Process Start-Service Start-Sleep Start-Transaction Start-Transcript Stop-Computer Stop-Job Stop-Process Stop-Service Stop-Transcript Suspend-Job Suspend-Service Tee-Object Test-ComputerSecureChannel Test-Connection Test-ModuleManifest Test-Path Test-PSSessionConfigurationFile Trace-Command Unblock-File Undo-Transaction Unregister-Event Unregister-PSSessionConfiguration Unregister-ScheduledJob Update-FormatData Update-Help Update-List Update-TypeData Use-Transaction Wait-Event Wait-Job Wait-Process Where-Object Write-Debug Write-Error Write-EventLog Write-Host Write-Output Write-Progress Write-Verbose Write-Warning Add-MDTPersistentDrive Disable-MDTMonitorService Enable-MDTMonitorService Get-MDTDeploymentShareStatistics Get-MDTMonitorData Get-MDTOperatingSystemCatalog Get-MDTPersistentDrive Import-MDTApplication Import-MDTDriver Import-MDTOperatingSystem Import-MDTPackage Import-MDTTaskSequence New-MDTDatabase Remove-MDTMonitorData Remove-MDTPersistentDrive Restore-MDTPersistentDrive Set-MDTMonitorData Test-MDTDeploymentShare Test-MDTMonitorData Update-MDTDatabaseSchema Update-MDTDeploymentShare Update-MDTLinkedDS Update-MDTMedia Update-MDTMedia Add-VamtProductKey Export-VamtData Find-VamtManagedMachine Get-VamtConfirmationId Get-VamtProduct Get-VamtProductKey Import-VamtData Initialize-VamtData Install-VamtConfirmationId Install-VamtProductActivation Install-VamtProductKey Update-VamtProduct",
+{begin:"=>"},e,f]}});b.registerLanguage("plaintext",function(a){return{disableAutodetect:!0}});b.registerLanguage("pony",function(a){var b={className:"type",begin:"\\b_?[A-Z][\\w]*",relevance:0},d={begin:a.IDENT_RE+"'",relevance:0};return{keywords:{keyword:"actor addressof and as be break class compile_error compile_intrinsicconsume continue delegate digestof do else elseif embed end errorfor fun if ifdef in interface is isnt lambda let match new not objector primitive recover repeat return struct then trait try type until use var where while with xor",
+meta:"iso val tag trn box ref",literal:"this false true"},contains:[{className:"class",beginKeywords:"class actor object",end:"$",contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE]},{className:"function",beginKeywords:"new fun",end:"=>",contains:[a.TITLE_MODE,{begin:/\(/,end:/\)/,contains:[b,d,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},{begin:/:/,endsWithParent:!0,contains:[b]},a.C_LINE_COMMENT_MODE]},b,{className:"string",begin:'"""',end:'"""',relevance:10},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE]},
+{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE],relevance:0},d,a.C_NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}});b.registerLanguage("powershell",function(a){var b={begin:"`[\\s\\S]",relevance:0},d={className:"variable",variants:[{begin:/\$[\w\d][\w\d_:]*/}]},e={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[b,d,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},f=a.inherit(a.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},
+{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]});return{aliases:["ps"],lexemes:/-?[A-z\.\-]+/,case_insensitive:!0,keywords:{keyword:"if else foreach return function do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch",
+built_in:"Add-Computer Add-Content Add-History Add-JobTrigger Add-Member Add-PSSnapin Add-Type Checkpoint-Computer Clear-Content Clear-EventLog Clear-History Clear-Host Clear-Item Clear-ItemProperty Clear-Variable Compare-Object Complete-Transaction Connect-PSSession Connect-WSMan Convert-Path ConvertFrom-Csv ConvertFrom-Json ConvertFrom-SecureString ConvertFrom-StringData ConvertTo-Csv ConvertTo-Html ConvertTo-Json ConvertTo-SecureString ConvertTo-Xml Copy-Item Copy-ItemProperty Debug-Process Disable-ComputerRestore Disable-JobTrigger Disable-PSBreakpoint Disable-PSRemoting Disable-PSSessionConfiguration Disable-WSManCredSSP Disconnect-PSSession Disconnect-WSMan Disable-ScheduledJob Enable-ComputerRestore Enable-JobTrigger Enable-PSBreakpoint Enable-PSRemoting Enable-PSSessionConfiguration Enable-ScheduledJob Enable-WSManCredSSP Enter-PSSession Exit-PSSession Export-Alias Export-Clixml Export-Console Export-Counter Export-Csv Export-FormatData Export-ModuleMember Export-PSSession ForEach-Object Format-Custom Format-List Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command Get-ComputerRestorePoint Get-Content Get-ControlPanelItem Get-Counter Get-Credential Get-Culture Get-Date Get-Event Get-EventLog Get-EventSubscriber Get-ExecutionPolicy Get-FormatData Get-Host Get-HotFix Get-Help Get-History Get-IseSnippet Get-Item Get-ItemProperty Get-Job Get-JobTrigger Get-Location Get-Member Get-Module Get-PfxCertificate Get-Process Get-PSBreakpoint Get-PSCallStack Get-PSDrive Get-PSProvider Get-PSSession Get-PSSessionConfiguration Get-PSSnapin Get-Random Get-ScheduledJob Get-ScheduledJobOption Get-Service Get-TraceSource Get-Transaction Get-TypeData Get-UICulture Get-Unique Get-Variable Get-Verb Get-WinEvent Get-WmiObject Get-WSManCredSSP Get-WSManInstance Group-Object Import-Alias Import-Clixml Import-Counter Import-Csv Import-IseSnippet Import-LocalizedData Import-PSSession Import-Module Invoke-AsWorkflow Invoke-Command Invoke-Expression Invoke-History Invoke-Item Invoke-RestMethod Invoke-WebRequest Invoke-WmiMethod Invoke-WSManAction Join-Path Limit-EventLog Measure-Command Measure-Object Move-Item Move-ItemProperty New-Alias New-Event New-EventLog New-IseSnippet New-Item New-ItemProperty New-JobTrigger New-Object New-Module New-ModuleManifest New-PSDrive New-PSSession New-PSSessionConfigurationFile New-PSSessionOption New-PSTransportOption New-PSWorkflowExecutionOption New-PSWorkflowSession New-ScheduledJobOption New-Service New-TimeSpan New-Variable New-WebServiceProxy New-WinEvent New-WSManInstance New-WSManSessionOption Out-Default Out-File Out-GridView Out-Host Out-Null Out-Printer Out-String Pop-Location Push-Location Read-Host Receive-Job Register-EngineEvent Register-ObjectEvent Register-PSSessionConfiguration Register-ScheduledJob Register-WmiEvent Remove-Computer Remove-Event Remove-EventLog Remove-Item Remove-ItemProperty Remove-Job Remove-JobTrigger Remove-Module Remove-PSBreakpoint Remove-PSDrive Remove-PSSession Remove-PSSnapin Remove-TypeData Remove-Variable Remove-WmiObject Remove-WSManInstance Rename-Computer Rename-Item Rename-ItemProperty Reset-ComputerMachinePassword Resolve-Path Restart-Computer Restart-Service Restore-Computer Resume-Job Resume-Service Save-Help Select-Object Select-String Select-Xml Send-MailMessage Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-JobTrigger Set-Location Set-PSBreakpoint Set-PSDebug Set-PSSessionConfiguration Set-ScheduledJob Set-ScheduledJobOption Set-Service Set-StrictMode Set-TraceSource Set-Variable Set-WmiInstance Set-WSManInstance Set-WSManQuickConfig Show-Command Show-ControlPanelItem Show-EventLog Sort-Object Split-Path Start-Job Start-Process Start-Service Start-Sleep Start-Transaction Start-Transcript Stop-Computer Stop-Job Stop-Process Stop-Service Stop-Transcript Suspend-Job Suspend-Service Tee-Object Test-ComputerSecureChannel Test-Connection Test-ModuleManifest Test-Path Test-PSSessionConfigurationFile Trace-Command Unblock-File Undo-Transaction Unregister-Event Unregister-PSSessionConfiguration Unregister-ScheduledJob Update-FormatData Update-Help Update-List Update-TypeData Use-Transaction Wait-Event Wait-Job Wait-Process Where-Object Write-Debug Write-Error Write-EventLog Write-Host Write-Output Write-Progress Write-Verbose Write-Warning Add-MDTPersistentDrive Disable-MDTMonitorService Enable-MDTMonitorService Get-MDTDeploymentShareStatistics Get-MDTMonitorData Get-MDTOperatingSystemCatalog Get-MDTPersistentDrive Import-MDTApplication Import-MDTDriver Import-MDTOperatingSystem Import-MDTPackage Import-MDTTaskSequence New-MDTDatabase Remove-MDTMonitorData Remove-MDTPersistentDrive Restore-MDTPersistentDrive Set-MDTMonitorData Test-MDTDeploymentShare Test-MDTMonitorData Update-MDTDatabaseSchema Update-MDTDeploymentShare Update-MDTLinkedDS Update-MDTMedia Update-MDTMedia Add-VamtProductKey Export-VamtData Find-VamtManagedMachine Get-VamtConfirmationId Get-VamtProduct Get-VamtProductKey Import-VamtData Initialize-VamtData Install-VamtConfirmationId Install-VamtProductActivation Install-VamtProductKey Update-VamtProduct",
 nomarkup:"-ne -eq -lt -gt -ge -le -not -like -notlike -match -notmatch -contains -notcontains -in -notin -replace"},contains:[b,a.NUMBER_MODE,e,{className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},{className:"literal",begin:/\$(null|true|false)\b/},d,f]}});b.registerLanguage("processing",function(a){return{keywords:{keyword:"BufferedReader PVector PFont PImage PGraphics HashMap boolean byte char color double float int long String Array FloatDict FloatList IntDict IntList JSONArray JSONObject Object StringDict StringList Table TableRow XML false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",
 literal:"P2D P3D HALF_PI PI QUARTER_PI TAU TWO_PI",title:"setup draw",built_in:"displayHeight displayWidth mouseY mouseX mousePressed pmouseX pmouseY key keyCode pixels focused frameCount frameRate height width size createGraphics beginDraw createShape loadShape PShape arc ellipse line point quad rect triangle bezier bezierDetail bezierPoint bezierTangent curve curveDetail curvePoint curveTangent curveTightness shape shapeMode beginContour beginShape bezierVertex curveVertex endContour endShape quadraticVertex vertex ellipseMode noSmooth rectMode smooth strokeCap strokeJoin strokeWeight mouseClicked mouseDragged mouseMoved mousePressed mouseReleased mouseWheel keyPressed keyPressedkeyReleased keyTyped print println save saveFrame day hour millis minute month second year background clear colorMode fill noFill noStroke stroke alpha blue brightness color green hue lerpColor red saturation modelX modelY modelZ screenX screenY screenZ ambient emissive shininess specular add createImage beginCamera camera endCamera frustum ortho perspective printCamera printProjection cursor frameRate noCursor exit loop noLoop popStyle pushStyle redraw binary boolean byte char float hex int str unbinary unhex join match matchAll nf nfc nfp nfs split splitTokens trim append arrayCopy concat expand reverse shorten sort splice subset box sphere sphereDetail createInput createReader loadBytes loadJSONArray loadJSONObject loadStrings loadTable loadXML open parseXML saveTable selectFolder selectInput beginRaw beginRecord createOutput createWriter endRaw endRecord PrintWritersaveBytes saveJSONArray saveJSONObject saveStream saveStrings saveXML selectOutput popMatrix printMatrix pushMatrix resetMatrix rotate rotateX rotateY rotateZ scale shearX shearY translate ambientLight directionalLight lightFalloff lights lightSpecular noLights normal pointLight spotLight image imageMode loadImage noTint requestImage tint texture textureMode textureWrap blend copy filter get loadPixels set updatePixels blendMode loadShader PShaderresetShader shader createFont loadFont text textFont textAlign textLeading textMode textSize textWidth textAscent textDescent abs ceil constrain dist exp floor lerp log mag map max min norm pow round sq sqrt acos asin atan atan2 cos degrees radians sin tan noise noiseDetail noiseSeed random randomGaussian randomSeed"},
 contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("profile",function(a){return{contains:[a.C_NUMBER_MODE,{begin:"[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}",end:":",excludeEnd:!0},{begin:"(ncalls|tottime|cumtime)",end:"$",keywords:"ncalls tottime|10 cumtime|10 filename",relevance:10},{begin:"function calls",end:"$",contains:[a.C_NUMBER_MODE],relevance:10},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\(",
 end:"\\)$",excludeBegin:!0,excludeEnd:!0,relevance:0}]}});b.registerLanguage("prolog",function(a){var b={begin:/\(/,end:/\)/,relevance:0},d={begin:/\[/,end:/\]/};a=[{begin:/[a-z][A-Za-z0-9_]*/,relevance:0},{className:"symbol",variants:[{begin:/[A-Z][a-zA-Z0-9_]*/},{begin:/_[A-Za-z0-9_]*/}],relevance:0},b,{begin:/:-/},d,{className:"comment",begin:/%/,end:/$/,contains:[a.PHRASAL_WORDS_MODE]},a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",begin:/`/,end:/`/,contains:[a.BACKSLASH_ESCAPE]},
-{className:"string",begin:/0'(\\'|.)/},{className:"string",begin:/0'\\s/},a.C_NUMBER_MODE];b.contains=a;d.contains=a;return{contains:a.concat([{begin:/\.$/}])}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group oneof",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",
-beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("puppet",function(a){var b=a.COMMENT("#","$"),d=a.inherit(a.TITLE_MODE,{begin:"([A-Za-z_]|::)(\\w|::)*"}),e={className:"variable",begin:"\\$([A-Za-z_]|::)(\\w|::)*"},f={className:"string",contains:[a.BACKSLASH_ESCAPE,
-e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]};return{aliases:["pp"],contains:[b,e,f,{beginKeywords:"class",end:"\\{|;",illegal:/=/,contains:[d,b]},{beginKeywords:"define",end:/\{/,contains:[{className:"section",begin:a.IDENT_RE,endsParent:!0}]},{begin:a.IDENT_RE+"\\s+\\{",returnBegin:!0,end:/\S/,contains:[{className:"keyword",begin:a.IDENT_RE},{begin:/\{/,end:/\}/,keywords:{keyword:"and case default else elsif false if in import enherits node or true undef unless main settings $string ",
-literal:"alias audit before loglevel noop require subscribe tag owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check en_address ip_address realname command environment hour monute month monthday special target weekday creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey sslverify mounted",
+{className:"string",begin:/0'(\\'|.)/},{className:"string",begin:/0'\\s/},a.C_NUMBER_MODE];b.contains=a;d.contains=a;return{contains:a.concat([{begin:/\.$/}])}});b.registerLanguage("properties",function(a){var b={end:"([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{case_insensitive:!0,illegal:/\S/,contains:[a.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",
+returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:b},{begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+([ \\t\\f]*[:=][ \\t\\f]*|[ \\t\\f]+)",returnBegin:!0,relevance:0,contains:[{className:"meta",begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:b},{className:"attr",relevance:0,begin:"([^\\\\:= \\t\\f\\n]|\\\\.)+[ \\t\\f]*$"}]}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group oneof",
+built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("puppet",
+function(a){var b=a.COMMENT("#","$"),d=a.inherit(a.TITLE_MODE,{begin:"([A-Za-z_]|::)(\\w|::)*"}),e={className:"variable",begin:"\\$([A-Za-z_]|::)(\\w|::)*"},f={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]};return{aliases:["pp"],contains:[b,e,f,{beginKeywords:"class",end:"\\{|;",illegal:/=/,contains:[d,b]},{beginKeywords:"define",end:/\{/,contains:[{className:"section",begin:a.IDENT_RE,endsParent:!0}]},{begin:a.IDENT_RE+"\\s+\\{",returnBegin:!0,
+end:/\S/,contains:[{className:"keyword",begin:a.IDENT_RE},{begin:/\{/,end:/\}/,keywords:{keyword:"and case default else elsif false if in import enherits node or true undef unless main settings $string ",literal:"alias audit before loglevel noop require subscribe tag owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check en_address ip_address realname command environment hour monute month monthday special target weekday creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey sslverify mounted",
 built_in:"architecture augeasversion blockdevices boardmanufacturer boardproductname boardserialnumber cfkey dhcp_servers domain ec2_ ec2_userdata facterversion filesystems ldom fqdn gid hardwareisa hardwaremodel hostname id|0 interfaces ipaddress ipaddress_ ipaddress6 ipaddress6_ iphostnumber is_virtual kernel kernelmajversion kernelrelease kernelversion kernelrelease kernelversion lsbdistcodename lsbdistdescription lsbdistid lsbdistrelease lsbmajdistrelease lsbminordistrelease lsbrelease macaddress macaddress_ macosx_buildversion macosx_productname macosx_productversion macosx_productverson_major macosx_productversion_minor manufacturer memoryfree memorysize netmask metmask_ network_ operatingsystem operatingsystemmajrelease operatingsystemrelease osfamily partitions path physicalprocessorcount processor processorcount productname ps puppetversion rubysitedir rubyversion selinux selinux_config_mode selinux_config_policy selinux_current_mode selinux_current_mode selinux_enforced selinux_policyversion serialnumber sp_ sshdsakey sshecdsakey sshrsakey swapencrypted swapfree swapsize timezone type uniqueid uptime uptime_days uptime_hours uptime_seconds uuid virtual vlans xendomains zfs_version zonenae zones zpool_version"},
 relevance:0,contains:[f,b,{begin:"[a-zA-Z_]+\\s*=>",returnBegin:!0,end:"=>",contains:[{className:"attr",begin:a.IDENT_RE}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},e]}],relevance:0}]}});b.registerLanguage("purebasic",function(a){return{aliases:["pb","pbi"],keywords:"And As Break CallDebugger Case CompilerCase CompilerDefault CompilerElse CompilerEndIf CompilerEndSelect CompilerError CompilerIf CompilerSelect Continue Data DataSection EndDataSection Debug DebugLevel Default Define Dim DisableASM DisableDebugger DisableExplicit Else ElseIf EnableASM EnableDebugger EnableExplicit End EndEnumeration EndIf EndImport EndInterface EndMacro EndProcedure EndSelect EndStructure EndStructureUnion EndWith Enumeration Extends FakeReturn For Next ForEach ForEver Global Gosub Goto If Import ImportC IncludeBinary IncludeFile IncludePath Interface Macro NewList Not Or ProcedureReturn Protected Prototype PrototypeC Read ReDim Repeat Until Restore Return Select Shared Static Step Structure StructureUnion Swap To Wend While With XIncludeFile XOr Procedure ProcedureC ProcedureCDLL ProcedureDLL Declare DeclareC DeclareCDLL DeclareDLL",
 contains:[a.COMMENT(";","$",{relevance:0}),{className:"function",begin:"\\b(Procedure|Declare)(C|CDLL|DLL)?\\b",end:"\\(",excludeEnd:!0,returnBegin:!0,contains:[{className:"keyword",begin:"(Procedure|Declare)(C|CDLL|DLL)?",excludeEnd:!0},{className:"type",begin:"\\.\\w*"},a.UNDERSCORE_TITLE_MODE]},{className:"string",begin:'(~)?"',end:'"',illegal:"\\n"},{className:"symbol",begin:"#[a-zA-Z_]\\w*\\$?"}]}});b.registerLanguage("python",function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",
-built_in:"Ellipsis NotImplemented"},d={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"subst",begin:/\{/,end:/\}/,keywords:b,illegal:/#/},f={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[d],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[d],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[d,e]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[d,e]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},
-{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},l={className:"params",begin:/\(/,end:/\)/,contains:["self",d,g,f]};e.contains=[f,g,d];return{aliases:["py","gyp"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[d,g,f,a.HASH_COMMENT_MODE,
-{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,l,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("q",function(a){return{aliases:["k","kdb"],keywords:{keyword:"do while select delete by update from",literal:"0b 1b",built_in:"neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum",
+built_in:"Ellipsis NotImplemented"},d={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"subst",begin:/\{/,end:/\}/,keywords:b,illegal:/#/},f={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[a.BACKSLASH_ESCAPE,d],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[a.BACKSLASH_ESCAPE,d],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[a.BACKSLASH_ESCAPE,d,e]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[a.BACKSLASH_ESCAPE,d,e]},{begin:/(u|r|ur)'/,
+end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[a.BACKSLASH_ESCAPE,e]},{begin:/(fr|rf|f)"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,e]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},k={className:"params",begin:/\(/,end:/\)/,contains:["self",d,g,f]};e.contains=
+[f,g,d];return{aliases:["py","gyp","ipython"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[d,g,f,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,k,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("q",function(a){return{aliases:["k","kdb"],keywords:{keyword:"do while select delete by update from",
+literal:"0b 1b",built_in:"neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum",
 type:"`float `double int `timestamp `timespan `datetime `time `boolean `symbol `char `byte `short `long `real `month `date `minute `second `guid"},lexemes:/(`?)[A-Za-z0-9_]+\b/,contains:[a.C_LINE_COMMENT_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE]}});b.registerLanguage("qml",function(a){var b={begin:"[a-zA-Z_][a-zA-Z0-9\\._]*\\s*{",end:"{",returnBegin:!0,relevance:0,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_][a-zA-Z0-9\\._]*"})]};return{aliases:["qt"],case_insensitive:!1,keywords:{keyword:"in of on if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import",
 literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Behavior bool color coordinate date double enumeration font geocircle georectangle geoshape int list matrix4x4 parent point quaternion real rect size string url variant vector2d vector3d vector4dPromise"},
 contains:[{className:"meta",begin:/^\s*['"]use (strict|asm)['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,
 a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{begin:/</,end:/>\s*[);\]]/,relevance:0,subLanguage:"xml"}],relevance:0},{className:"keyword",begin:"\\bsignal\\b",starts:{className:"string",end:"(\\(|:|=|;|,|//|/\\*|$)",returnEnd:!0}},{className:"keyword",begin:"\\bproperty\\b",starts:{className:"string",end:"(:|=|;|,|//|/\\*|$)",returnEnd:!0}},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,
 end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}],illegal:/\[|%/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"attribute",begin:"\\bid\\s*:",starts:{className:"string",end:"[a-zA-Z_][a-zA-Z0-9\\._]*",returnEnd:!1}},{begin:"[a-zA-Z_][a-zA-Z0-9\\._]*\\s*:",returnBegin:!0,contains:[{className:"attribute",begin:"[a-zA-Z_][a-zA-Z0-9\\._]*",end:"\\s*:",excludeEnd:!0,relevance:0}],relevance:0},b],illegal:/#/}});b.registerLanguage("r",function(a){return{contains:[a.HASH_COMMENT_MODE,
 {begin:"([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*",lexemes:"([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*",keywords:{keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",
-relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}});b.registerLanguage("rib",function(a){return{keywords:"ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry Hider Hyperboloid Identity Illuminate Imager Interior LightSource MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd TransformPoints Translate TrimCurve WorldBegin WorldEnd",
+relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}});b.registerLanguage("reasonml",function(a){var b="("+function(a){return a.map(function(a){return a.split("").map(function(a){return"\\"+
+a}).join("")}).join("|")}("|| && ++ ** +. * / *. /. ... |>".split(" "))+"|==|===)",d="\\s+"+b+"\\s+",e={keyword:"and as asr assert begin class constraint do done downto else end exception externalfor fun function functor if in include inherit initializerland lazy let lor lsl lsr lxor match method mod module mutable new nonrecobject of open or private rec sig struct then to try type val virtual when while with",built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 ref string unit ",
+literal:"true false"},f={className:"number",relevance:0,variants:[{begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)"},{begin:"\\(\\-\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)\\)"}]},g={className:"operator",relevance:0,begin:b};b=[{className:"identifier",relevance:0,begin:"~?[a-z$_][0-9a-zA-Z$_]*"},g,f];var k=[a.QUOTE_STRING_MODE,g,{className:"module",
+begin:"\\b`?[A-Z$_][0-9a-zA-Z$_]*",returnBegin:!0,end:".",contains:[{className:"identifier",begin:"`?[A-Z$_][0-9a-zA-Z$_]*",relevance:0}]}],m=[{className:"module",begin:"\\b`?[A-Z$_][0-9a-zA-Z$_]*",returnBegin:!0,end:".",relevance:0,contains:[{className:"identifier",begin:"`?[A-Z$_][0-9a-zA-Z$_]*",relevance:0}]}],l={className:"function",relevance:0,keywords:e,variants:[{begin:"\\s(\\(\\.?.*?\\)|~?[a-z$_][0-9a-zA-Z$_]*)\\s*=>",end:"\\s*=>",returnBegin:!0,relevance:0,contains:[{className:"params",variants:[{begin:"~?[a-z$_][0-9a-zA-Z$_]*"},
+{begin:"~?[a-z$_][0-9a-zA-Z$_]*(s*:s*[a-z$_][0-9a-z$_]*((s*('?[a-z$_][0-9a-z$_]*s*(,'?[a-z$_][0-9a-z$_]*)*)?s*))?)?(s*:s*[a-z$_][0-9a-z$_]*((s*('?[a-z$_][0-9a-z$_]*s*(,'?[a-z$_][0-9a-z$_]*)*)?s*))?)?"},{begin:/\(\s*\)/}]}]},{begin:"\\s\\(\\.?[^;\\|]*\\)\\s*=>",end:"\\s=>",returnBegin:!0,relevance:0,contains:[{className:"params",relevance:0,variants:[{begin:"~?[a-z$_][0-9a-zA-Z$_]*",end:"(,|\\n|\\))",relevance:0,contains:[g,{className:"typing",begin:":",end:"(,|\\n)",returnBegin:!0,relevance:0,contains:m}]}]}]},
+{begin:"\\(\\.\\s~?[a-z$_][0-9a-zA-Z$_]*\\)\\s*=>"}]};k.push(l);var n={className:"constructor",begin:"`?[A-Z$_][0-9a-zA-Z$_]*\\(",end:"\\)",illegal:"\\n",keywords:e,contains:[a.QUOTE_STRING_MODE,g,{className:"params",begin:"\\b~?[a-z$_][0-9a-zA-Z$_]*"}]};g={className:"pattern-match",begin:"\\|",returnBegin:!0,keywords:e,end:"=>",relevance:0,contains:[n,g,{relevance:0,className:"constructor",begin:"`?[A-Z$_][0-9a-zA-Z$_]*"}]};var h={className:"module-access",keywords:e,returnBegin:!0,variants:[{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+~?[a-z$_][0-9a-zA-Z$_]*"},
+{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+\\(",end:"\\)",returnBegin:!0,contains:[l,{begin:"\\(",end:"\\)",skip:!0}].concat(k)},{begin:"\\b(`?[A-Z$_][0-9a-zA-Z$_]*\\.)+{",end:"}"}],contains:k};m.push(h);return{aliases:["re"],keywords:e,illegal:"(:\\-|:=|\\${|\\+=)",contains:[a.COMMENT("/\\*","\\*/",{illegal:"^(\\#,\\/\\/)"}),{className:"character",begin:"'(\\\\[^']+|[^'])'",illegal:"\\n",relevance:0},a.QUOTE_STRING_MODE,{className:"literal",begin:"\\(\\)",relevance:0},{className:"literal",begin:"\\[\\|",
+end:"\\|\\]",relevance:0,contains:b},{className:"literal",begin:"\\[",end:"\\]",relevance:0,contains:b},n,{className:"operator",begin:d,illegal:"\\-\\->",relevance:0},f,a.C_LINE_COMMENT_MODE,g,l,{className:"module-def",begin:"\\bmodule\\s+~?[a-z$_][0-9a-zA-Z$_]*\\s+`?[A-Z$_][0-9a-zA-Z$_]*\\s+=\\s+{",end:"}",returnBegin:!0,keywords:e,relevance:0,contains:[{className:"module",relevance:0,begin:"`?[A-Z$_][0-9a-zA-Z$_]*"},{begin:"{",end:"}",skip:!0}].concat(k)},h]}});b.registerLanguage("rib",function(a){return{keywords:"ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry Hider Hyperboloid Identity Illuminate Imager Interior LightSource MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd TransformPoints Translate TrimCurve WorldBegin WorldEnd",
 illegal:"</",contains:[a.HASH_COMMENT_MODE,a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}});b.registerLanguage("roboconf",function(a){var b={className:"attribute",begin:/[a-zA-Z-_]+/,end:/\s*:/,excludeEnd:!0,starts:{end:";",relevance:0,contains:[{className:"variable",begin:/\.[a-zA-Z-_]+/},{className:"keyword",begin:/\(optional\)/}]}};return{aliases:["graph","instances"],case_insensitive:!0,keywords:"import",contains:[{begin:"^facet [a-zA-Z-_][^\\n{]+\\{",end:"}",keywords:"facet",contains:[b,
 a.HASH_COMMENT_MODE]},{begin:"^\\s*instance of [a-zA-Z-_][^\\n{]+\\{",end:"}",keywords:"name count channels instance-data instance-state instance of",illegal:/\S/,contains:["self",b,a.HASH_COMMENT_MODE]},{begin:"^[a-zA-Z-_][^\\n{]+\\{",end:"}",contains:[b,a.HASH_COMMENT_MODE]},a.HASH_COMMENT_MODE]}});b.registerLanguage("routeros",function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},d={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,
 b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]},e={className:"string",begin:/'/,end:/'/};return{aliases:["routeros","mikrotik"],case_insensitive:!0,lexemes:/:?[\w-]+/,keywords:{literal:"true false yes no nothing nil null",keyword:"foreach do while for if from to step else on-error and or not in :foreach :do :while :for :if :from :to :step :else :on-error :and :or :not :in :global :local :beep :delay :put :len :typeof :pick :log :time :set :find :environment :terminal :error :execute :parse :resolve :toarray :tobool :toid :toip :toip6 :tonum :tostr :totime"},
@@ -350,12 +390,15 @@
 lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0o([0-7_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},
 {begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([ui](8|16|32|64|128|size)|f(32|64))?"}],relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct union",end:"{",
 contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+"::",keywords:{built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}},
-{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},d={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},e={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
+{begin:"->"}]}});b.registerLanguage("sas",function(a){return{aliases:["sas","SAS"],case_insensitive:!0,keywords:{literal:"null missing _all_ _automatic_ _character_ _infile_ _n_ _name_ _null_ _numeric_ _user_ _webout_",meta:"do if then else end until while abort array attrib by call cards cards4 catname continue datalines datalines4 delete delim delimiter display dm drop endsas error file filename footnote format goto in infile informat input keep label leave length libname link list lostcard merge missing modify options output out page put redirect remove rename replace retain return select set skip startsas stop title update waitsas where window x systask add and alter as cascade check create delete describe distinct drop foreign from group having index insert into in key like message modify msgtype not null on or order primary references reset restrict select set table unique update validate view where"},
+contains:[{className:"keyword",begin:/^\s*(proc [\w\d_]+|data|run|quit)[\s;]/},{className:"variable",begin:/&[a-zA-Z_&][a-zA-Z0-9_]*\.?/},{className:"emphasis",begin:/^\s*datalines|cards.*;/,end:/^\s*;\s*$/},{className:"built_in",begin:"%(bquote|nrbquote|cmpres|qcmpres|compstor|datatyp|display|do|else|end|eval|global|goto|if|index|input|keydef|label|left|length|let|local|lowcase|macro|mend|nrbquote|nrquote|nrstr|put|qcmpres|qleft|qlowcase|qscan|qsubstr|qsysfunc|qtrim|quote|qupcase|scan|str|substr|superq|syscall|sysevalf|sysexec|sysfunc|sysget|syslput|sysprod|sysrc|sysrput|then|to|trim|unquote|until|upcase|verify|while|window)"},
+{className:"name",begin:/%[a-zA-Z_][a-zA-Z_0-9]*/},{className:"meta",begin:"[^%](abs|addr|airy|arcos|arsin|atan|attrc|attrn|band|betainv|blshift|bnot|bor|brshift|bxor|byte|cdf|ceil|cexist|cinv|close|cnonct|collate|compbl|compound|compress|cos|cosh|css|curobs|cv|daccdb|daccdbsl|daccsl|daccsyd|dacctab|dairy|date|datejul|datepart|datetime|day|dclose|depdb|depdbsl|depdbsl|depsl|depsl|depsyd|depsyd|deptab|deptab|dequote|dhms|dif|digamma|dim|dinfo|dnum|dopen|doptname|doptnum|dread|dropnote|dsname|erf|erfc|exist|exp|fappend|fclose|fcol|fdelete|fetch|fetchobs|fexist|fget|fileexist|filename|fileref|finfo|finv|fipname|fipnamel|fipstate|floor|fnonct|fnote|fopen|foptname|foptnum|fpoint|fpos|fput|fread|frewind|frlen|fsep|fuzz|fwrite|gaminv|gamma|getoption|getvarc|getvarn|hbound|hms|hosthelp|hour|ibessel|index|indexc|indexw|input|inputc|inputn|int|intck|intnx|intrr|irr|jbessel|juldate|kurtosis|lag|lbound|left|length|lgamma|libname|libref|log|log10|log2|logpdf|logpmf|logsdf|lowcase|max|mdy|mean|min|minute|mod|month|mopen|mort|n|netpv|nmiss|normal|note|npv|open|ordinal|pathname|pdf|peek|peekc|pmf|point|poisson|poke|probbeta|probbnml|probchi|probf|probgam|probhypr|probit|probnegb|probnorm|probt|put|putc|putn|qtr|quote|ranbin|rancau|ranexp|rangam|range|rank|rannor|ranpoi|rantbl|rantri|ranuni|repeat|resolve|reverse|rewind|right|round|saving|scan|sdf|second|sign|sin|sinh|skewness|soundex|spedis|sqrt|std|stderr|stfips|stname|stnamel|substr|sum|symget|sysget|sysmsg|sysprod|sysrc|system|tan|tanh|time|timepart|tinv|tnonct|today|translate|tranwrd|trigamma|trim|trimn|trunc|uniform|upcase|uss|var|varfmt|varinfmt|varlabel|varlen|varname|varnum|varray|varrayx|vartype|verify|vformat|vformatd|vformatdx|vformatn|vformatnx|vformatw|vformatwx|vformatx|vinarray|vinarrayx|vinformat|vinformatd|vinformatdx|vinformatn|vinformatnx|vinformatw|vinformatwx|vinformatx|vlabel|vlabelx|vlength|vlengthx|vname|vnamex|vtype|vtypex|weekday|year|yyq|zipfips|zipname|zipnamel|zipstate)[(]"},
+{className:"string",variants:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},a.COMMENT("\\*",";"),a.C_BLOCK_COMMENT_MODE]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},d={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},e={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
 contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},d,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[e]},{className:"class",beginKeywords:"class object trait type",
 end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},e]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("scheme",function(a){var b={className:"literal",begin:"(#t|#f|#\\\\[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+|#\\\\.)"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+([./]\\d+)?",
-relevance:0},{begin:"(\\-|\\+)?\\d+([./]\\d+)?[+\\-](\\-|\\+)?\\d+([./]\\d+)?i",relevance:0},{begin:"#b[0-1]+(/[0-1]+)?"},{begin:"#o[0-7]+(/[0-7]+)?"},{begin:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},e=a.QUOTE_STRING_MODE;a=[a.COMMENT(";","$",{relevance:0}),a.COMMENT("#\\|","\\|#")];var f={begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",relevance:0},g={className:"symbol",begin:"'[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+"},l={endsWithParent:!0,relevance:0},k={variants:[{begin:/'/},{begin:"`"}],contains:[{begin:"\\(",
-end:"\\)",contains:["self",b,e,d,f,g]}]},m={className:"name",begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",lexemes:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",keywords:{"builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"}};
-m={variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}],contains:[{begin:/lambda/,endsWithParent:!0,returnBegin:!0,contains:[m,{begin:/\(/,end:/\)/,endsParent:!0,contains:[f]}]},m,l]};l.contains=[b,d,e,f,g,k,m].concat(a);return{illegal:/\S/,contains:[{className:"meta",begin:"^#!",end:"$"},d,e,g,k,m].concat(a)}});b.registerLanguage("scilab",function(a){var b=[a.C_NUMBER_MODE,{className:"string",begin:"'|\"",end:"'|\"",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]}];return{aliases:["sci"],lexemes:/%?\w+/,
+relevance:0},{begin:"(\\-|\\+)?\\d+([./]\\d+)?[+\\-](\\-|\\+)?\\d+([./]\\d+)?i",relevance:0},{begin:"#b[0-1]+(/[0-1]+)?"},{begin:"#o[0-7]+(/[0-7]+)?"},{begin:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},e=a.QUOTE_STRING_MODE;a=[a.COMMENT(";","$",{relevance:0}),a.COMMENT("#\\|","\\|#")];var f={begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",relevance:0},g={className:"symbol",begin:"'[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+"},k={endsWithParent:!0,relevance:0},m={variants:[{begin:/'/},{begin:"`"}],contains:[{begin:"\\(",
+end:"\\)",contains:["self",b,e,d,f,g]}]},l={className:"name",begin:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",lexemes:"[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",keywords:{"builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"}};
+l={variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}],contains:[{begin:/lambda/,endsWithParent:!0,returnBegin:!0,contains:[l,{begin:/\(/,end:/\)/,endsParent:!0,contains:[f]}]},l,k]};k.contains=[b,d,e,f,g,m,l].concat(a);return{illegal:/\S/,contains:[{className:"meta",begin:"^#!",end:"$"},d,e,g,m,l].concat(a)}});b.registerLanguage("scilab",function(a){var b=[a.C_NUMBER_MODE,{className:"string",begin:"'|\"",end:"'|\"",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]}];return{aliases:["sci"],lexemes:/%?\w+/,
 keywords:{keyword:"abort break case clear catch continue do elseif else endfunction end for function global if pause return resume select try then while",literal:"%f %F %t %T %pi %eps %inf %nan %e %i %z %s",built_in:"abs and acos asin atan ceil cd chdir clearglobal cosh cos cumprod deff disp error exec execstr exists exp eye gettext floor fprintf fread fsolve imag isdef isempty isinfisnan isvector lasterror length load linspace list listfiles log10 log2 log max min msprintf mclose mopen ones or pathconvert poly printf prod pwd rand real round sinh sin size gsort sprintf sqrt strcat strcmps tring sum system tanh tan type typename warning zeros matrix"},
 illegal:'("|#|/\\*|\\s+/\\w+)',contains:[{className:"function",beginKeywords:"function",end:"$",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{begin:"[a-zA-Z_][a-zA-Z_0-9]*('+[\\.']*|[\\.']+)",end:"",relevance:0},{begin:"\\[",end:"\\]'*[\\.']*",relevance:0,contains:b},a.COMMENT("//","$")].concat(b)}});b.registerLanguage("scss",function(a){var b={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},d={className:"number",begin:"#[0-9A-Fa-f]+"};return{case_insensitive:!0,
 illegal:"[=/|']",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",
@@ -370,9 +413,9 @@
 begin:/\[(\|\|)?\]|\(\)/,relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*"},a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",
 relevance:0},{begin:/[-=]>/}]}});b.registerLanguage("sqf",function(a){var b=a.getLanguage("cpp").exports;return{aliases:["sqf"],case_insensitive:!0,keywords:{keyword:"case catch default do else exit exitWith for forEach from if private switch then throw to try waitUntil while with",built_in:"abs accTime acos action actionIDs actionKeys actionKeysImages actionKeysNames actionKeysNamesArray actionName actionParams activateAddons activatedAddons activateKey add3DENConnection add3DENEventHandler add3DENLayer addAction addBackpack addBackpackCargo addBackpackCargoGlobal addBackpackGlobal addCamShake addCuratorAddons addCuratorCameraArea addCuratorEditableObjects addCuratorEditingArea addCuratorPoints addEditorObject addEventHandler addForce addGoggles addGroupIcon addHandgunItem addHeadgear addItem addItemCargo addItemCargoGlobal addItemPool addItemToBackpack addItemToUniform addItemToVest addLiveStats addMagazine addMagazineAmmoCargo addMagazineCargo addMagazineCargoGlobal addMagazineGlobal addMagazinePool addMagazines addMagazineTurret addMenu addMenuItem addMissionEventHandler addMPEventHandler addMusicEventHandler addOwnedMine addPlayerScores addPrimaryWeaponItem addPublicVariableEventHandler addRating addResources addScore addScoreSide addSecondaryWeaponItem addSwitchableUnit addTeamMember addToRemainsCollector addTorque addUniform addVehicle addVest addWaypoint addWeapon addWeaponCargo addWeaponCargoGlobal addWeaponGlobal addWeaponItem addWeaponPool addWeaponTurret admin agent agents AGLToASL aimedAtTarget aimPos airDensityRTD airplaneThrottle airportSide AISFinishHeal alive all3DENEntities allAirports allControls allCurators allCutLayers allDead allDeadMen allDisplays allGroups allMapMarkers allMines allMissionObjects allow3DMode allowCrewInImmobile allowCuratorLogicIgnoreAreas allowDamage allowDammage allowFileOperations allowFleeing allowGetIn allowSprint allPlayers allSimpleObjects allSites allTurrets allUnits allUnitsUAV allVariables ammo ammoOnPylon and animate animateBay animateDoor animatePylon animateSource animationNames animationPhase animationSourcePhase animationState append apply armoryPoints arrayIntersect asin ASLToAGL ASLToATL assert assignAsCargo assignAsCargoIndex assignAsCommander assignAsDriver assignAsGunner assignAsTurret assignCurator assignedCargo assignedCommander assignedDriver assignedGunner assignedItems assignedTarget assignedTeam assignedVehicle assignedVehicleRole assignItem assignTeam assignToAirport atan atan2 atg ATLToASL attachedObject attachedObjects attachedTo attachObject attachTo attackEnabled backpack backpackCargo backpackContainer backpackItems backpackMagazines backpackSpaceFor behaviour benchmark binocular boundingBox boundingBoxReal boundingCenter breakOut breakTo briefingName buildingExit buildingPos buttonAction buttonSetAction cadetMode call callExtension camCommand camCommit camCommitPrepared camCommitted camConstuctionSetParams camCreate camDestroy cameraEffect cameraEffectEnableHUD cameraInterest cameraOn cameraView campaignConfigFile camPreload camPreloaded camPrepareBank camPrepareDir camPrepareDive camPrepareFocus camPrepareFov camPrepareFovRange camPreparePos camPrepareRelPos camPrepareTarget camSetBank camSetDir camSetDive camSetFocus camSetFov camSetFovRange camSetPos camSetRelPos camSetTarget camTarget camUseNVG canAdd canAddItemToBackpack canAddItemToUniform canAddItemToVest cancelSimpleTaskDestination canFire canMove canSlingLoad canStand canSuspend canTriggerDynamicSimulation canUnloadInCombat canVehicleCargo captive captiveNum cbChecked cbSetChecked ceil channelEnabled cheatsEnabled checkAIFeature checkVisibility className clearAllItemsFromBackpack clearBackpackCargo clearBackpackCargoGlobal clearGroupIcons clearItemCargo clearItemCargoGlobal clearItemPool clearMagazineCargo clearMagazineCargoGlobal clearMagazinePool clearOverlay clearRadio clearWeaponCargo clearWeaponCargoGlobal clearWeaponPool clientOwner closeDialog closeDisplay closeOverlay collapseObjectTree collect3DENHistory collectiveRTD combatMode commandArtilleryFire commandChat commander commandFire commandFollow commandFSM commandGetOut commandingMenu commandMove commandRadio commandStop commandSuppressiveFire commandTarget commandWatch comment commitOverlay compile compileFinal completedFSM composeText configClasses configFile configHierarchy configName configProperties configSourceAddonList configSourceMod configSourceModList confirmSensorTarget connectTerminalToUAV controlsGroupCtrl copyFromClipboard copyToClipboard copyWaypoints cos count countEnemy countFriendly countSide countType countUnknown create3DENComposition create3DENEntity createAgent createCenter createDialog createDiaryLink createDiaryRecord createDiarySubject createDisplay createGearDialog createGroup createGuardedPoint createLocation createMarker createMarkerLocal createMenu createMine createMissionDisplay createMPCampaignDisplay createSimpleObject createSimpleTask createSite createSoundSource createTask createTeam createTrigger createUnit createVehicle createVehicleCrew createVehicleLocal crew ctAddHeader ctAddRow ctClear ctCurSel ctData ctFindHeaderRows ctFindRowHeader ctHeaderControls ctHeaderCount ctRemoveHeaders ctRemoveRows ctrlActivate ctrlAddEventHandler ctrlAngle ctrlAutoScrollDelay ctrlAutoScrollRewind ctrlAutoScrollSpeed ctrlChecked ctrlClassName ctrlCommit ctrlCommitted ctrlCreate ctrlDelete ctrlEnable ctrlEnabled ctrlFade ctrlHTMLLoaded ctrlIDC ctrlIDD ctrlMapAnimAdd ctrlMapAnimClear ctrlMapAnimCommit ctrlMapAnimDone ctrlMapCursor ctrlMapMouseOver ctrlMapScale ctrlMapScreenToWorld ctrlMapWorldToScreen ctrlModel ctrlModelDirAndUp ctrlModelScale ctrlParent ctrlParentControlsGroup ctrlPosition ctrlRemoveAllEventHandlers ctrlRemoveEventHandler ctrlScale ctrlSetActiveColor ctrlSetAngle ctrlSetAutoScrollDelay ctrlSetAutoScrollRewind ctrlSetAutoScrollSpeed ctrlSetBackgroundColor ctrlSetChecked ctrlSetEventHandler ctrlSetFade ctrlSetFocus ctrlSetFont ctrlSetFontH1 ctrlSetFontH1B ctrlSetFontH2 ctrlSetFontH2B ctrlSetFontH3 ctrlSetFontH3B ctrlSetFontH4 ctrlSetFontH4B ctrlSetFontH5 ctrlSetFontH5B ctrlSetFontH6 ctrlSetFontH6B ctrlSetFontHeight ctrlSetFontHeightH1 ctrlSetFontHeightH2 ctrlSetFontHeightH3 ctrlSetFontHeightH4 ctrlSetFontHeightH5 ctrlSetFontHeightH6 ctrlSetFontHeightSecondary ctrlSetFontP ctrlSetFontPB ctrlSetFontSecondary ctrlSetForegroundColor ctrlSetModel ctrlSetModelDirAndUp ctrlSetModelScale ctrlSetPixelPrecision ctrlSetPosition ctrlSetScale ctrlSetStructuredText ctrlSetText ctrlSetTextColor ctrlSetTooltip ctrlSetTooltipColorBox ctrlSetTooltipColorShade ctrlSetTooltipColorText ctrlShow ctrlShown ctrlText ctrlTextHeight ctrlTextWidth ctrlType ctrlVisible ctRowControls ctRowCount ctSetCurSel ctSetData ctSetHeaderTemplate ctSetRowTemplate ctSetValue ctValue curatorAddons curatorCamera curatorCameraArea curatorCameraAreaCeiling curatorCoef curatorEditableObjects curatorEditingArea curatorEditingAreaType curatorMouseOver curatorPoints curatorRegisteredObjects curatorSelected curatorWaypointCost current3DENOperation currentChannel currentCommand currentMagazine currentMagazineDetail currentMagazineDetailTurret currentMagazineTurret currentMuzzle currentNamespace currentTask currentTasks currentThrowable currentVisionMode currentWaypoint currentWeapon currentWeaponMode currentWeaponTurret currentZeroing cursorObject cursorTarget customChat customRadio cutFadeOut cutObj cutRsc cutText damage date dateToNumber daytime deActivateKey debriefingText debugFSM debugLog deg delete3DENEntities deleteAt deleteCenter deleteCollection deleteEditorObject deleteGroup deleteGroupWhenEmpty deleteIdentity deleteLocation deleteMarker deleteMarkerLocal deleteRange deleteResources deleteSite deleteStatus deleteTeam deleteVehicle deleteVehicleCrew deleteWaypoint detach detectedMines diag_activeMissionFSMs diag_activeScripts diag_activeSQFScripts diag_activeSQSScripts diag_captureFrame diag_captureFrameToFile diag_captureSlowFrame diag_codePerformance diag_drawMode diag_enable diag_enabled diag_fps diag_fpsMin diag_frameNo diag_lightNewLoad diag_list diag_log diag_logSlowFrame diag_mergeConfigFile diag_recordTurretLimits diag_setLightNew diag_tickTime diag_toggle dialog diarySubjectExists didJIP didJIPOwner difficulty difficultyEnabled difficultyEnabledRTD difficultyOption direction directSay disableAI disableCollisionWith disableConversation disableDebriefingStats disableMapIndicators disableNVGEquipment disableRemoteSensors disableSerialization disableTIEquipment disableUAVConnectability disableUserInput displayAddEventHandler displayCtrl displayParent displayRemoveAllEventHandlers displayRemoveEventHandler displaySetEventHandler dissolveTeam distance distance2D distanceSqr distributionRegion do3DENAction doArtilleryFire doFire doFollow doFSM doGetOut doMove doorPhase doStop doSuppressiveFire doTarget doWatch drawArrow drawEllipse drawIcon drawIcon3D drawLine drawLine3D drawLink drawLocation drawPolygon drawRectangle drawTriangle driver drop dynamicSimulationDistance dynamicSimulationDistanceCoef dynamicSimulationEnabled dynamicSimulationSystemEnabled echo edit3DENMissionAttributes editObject editorSetEventHandler effectiveCommander emptyPositions enableAI enableAIFeature enableAimPrecision enableAttack enableAudioFeature enableAutoStartUpRTD enableAutoTrimRTD enableCamShake enableCaustics enableChannel enableCollisionWith enableCopilot enableDebriefingStats enableDiagLegend enableDynamicSimulation enableDynamicSimulationSystem enableEndDialog enableEngineArtillery enableEnvironment enableFatigue enableGunLights enableInfoPanelComponent enableIRLasers enableMimics enablePersonTurret enableRadio enableReload enableRopeAttach enableSatNormalOnDetail enableSaving enableSentences enableSimulation enableSimulationGlobal enableStamina enableTeamSwitch enableTraffic enableUAVConnectability enableUAVWaypoints enableVehicleCargo enableVehicleSensor enableWeaponDisassembly endLoadingScreen endMission engineOn enginesIsOnRTD enginesRpmRTD enginesTorqueRTD entities environmentEnabled estimatedEndServerTime estimatedTimeLeft evalObjectArgument everyBackpack everyContainer exec execEditorScript execFSM execVM exp expectedDestination exportJIPMessages eyeDirection eyePos face faction fadeMusic fadeRadio fadeSound fadeSpeech failMission fillWeaponsFromPool find findCover findDisplay findEditorObject findEmptyPosition findEmptyPositionReady findIf findNearestEnemy finishMissionInit finite fire fireAtTarget firstBackpack flag flagAnimationPhase flagOwner flagSide flagTexture fleeing floor flyInHeight flyInHeightASL fog fogForecast fogParams forceAddUniform forcedMap forceEnd forceFlagTexture forceFollowRoad forceMap forceRespawn forceSpeed forceWalk forceWeaponFire forceWeatherChange forEachMember forEachMemberAgent forEachMemberTeam forgetTarget format formation formationDirection formationLeader formationMembers formationPosition formationTask formatText formLeader freeLook fromEditor fuel fullCrew gearIDCAmmoCount gearSlotAmmoCount gearSlotData get3DENActionState get3DENAttribute get3DENCamera get3DENConnections get3DENEntity get3DENEntityID get3DENGrid get3DENIconsVisible get3DENLayerEntities get3DENLinesVisible get3DENMissionAttribute get3DENMouseOver get3DENSelected getAimingCoef getAllEnvSoundControllers getAllHitPointsDamage getAllOwnedMines getAllSoundControllers getAmmoCargo getAnimAimPrecision getAnimSpeedCoef getArray getArtilleryAmmo getArtilleryComputerSettings getArtilleryETA getAssignedCuratorLogic getAssignedCuratorUnit getBackpackCargo getBleedingRemaining getBurningValue getCameraViewDirection getCargoIndex getCenterOfMass getClientState getClientStateNumber getCompatiblePylonMagazines getConnectedUAV getContainerMaxLoad getCursorObjectParams getCustomAimCoef getDammage getDescription getDir getDirVisual getDLCAssetsUsage getDLCAssetsUsageByName getDLCs getEditorCamera getEditorMode getEditorObjectScope getElevationOffset getEnvSoundController getFatigue getForcedFlagTexture getFriend getFSMVariable getFuelCargo getGroupIcon getGroupIconParams getGroupIcons getHideFrom getHit getHitIndex getHitPointDamage getItemCargo getMagazineCargo getMarkerColor getMarkerPos getMarkerSize getMarkerType getMass getMissionConfig getMissionConfigValue getMissionDLCs getMissionLayerEntities getModelInfo getMousePosition getMusicPlayedTime getNumber getObjectArgument getObjectChildren getObjectDLC getObjectMaterials getObjectProxy getObjectTextures getObjectType getObjectViewDistance getOxygenRemaining getPersonUsedDLCs getPilotCameraDirection getPilotCameraPosition getPilotCameraRotation getPilotCameraTarget getPlateNumber getPlayerChannel getPlayerScores getPlayerUID getPos getPosASL getPosASLVisual getPosASLW getPosATL getPosATLVisual getPosVisual getPosWorld getPylonMagazines getRelDir getRelPos getRemoteSensorsDisabled getRepairCargo getResolution getShadowDistance getShotParents getSlingLoad getSoundController getSoundControllerResult getSpeed getStamina getStatValue getSuppression getTerrainGrid getTerrainHeightASL getText getTotalDLCUsageTime getUnitLoadout getUnitTrait getUserMFDText getUserMFDvalue getVariable getVehicleCargo getWeaponCargo getWeaponSway getWingsOrientationRTD getWingsPositionRTD getWPPos glanceAt globalChat globalRadio goggles goto group groupChat groupFromNetId groupIconSelectable groupIconsVisible groupId groupOwner groupRadio groupSelectedUnits groupSelectUnit gunner gusts halt handgunItems handgunMagazine handgunWeapon handsHit hasInterface hasPilotCamera hasWeapon hcAllGroups hcGroupParams hcLeader hcRemoveAllGroups hcRemoveGroup hcSelected hcSelectGroup hcSetGroup hcShowBar hcShownBar headgear hideBody hideObject hideObjectGlobal hideSelection hint hintC hintCadet hintSilent hmd hostMission htmlLoad HUDMovementLevels humidity image importAllGroups importance in inArea inAreaArray incapacitatedState inflame inflamed infoPanel infoPanelComponentEnabled infoPanelComponents infoPanels inGameUISetEventHandler inheritsFrom initAmbientLife inPolygon inputAction inRangeOfArtillery insertEditorObject intersect is3DEN is3DENMultiplayer isAbleToBreathe isAgent isArray isAutoHoverOn isAutonomous isAutotest isBleeding isBurning isClass isCollisionLightOn isCopilotEnabled isDamageAllowed isDedicated isDLCAvailable isEngineOn isEqualTo isEqualType isEqualTypeAll isEqualTypeAny isEqualTypeArray isEqualTypeParams isFilePatchingEnabled isFlashlightOn isFlatEmpty isForcedWalk isFormationLeader isGroupDeletedWhenEmpty isHidden isInRemainsCollector isInstructorFigureEnabled isIRLaserOn isKeyActive isKindOf isLaserOn isLightOn isLocalized isManualFire isMarkedForCollection isMultiplayer isMultiplayerSolo isNil isNull isNumber isObjectHidden isObjectRTD isOnRoad isPipEnabled isPlayer isRealTime isRemoteExecuted isRemoteExecutedJIP isServer isShowing3DIcons isSimpleObject isSprintAllowed isStaminaEnabled isSteamMission isStreamFriendlyUIEnabled isText isTouchingGround isTurnedOut isTutHintsEnabled isUAVConnectable isUAVConnected isUIContext isUniformAllowed isVehicleCargo isVehicleRadarOn isVehicleSensorEnabled isWalking isWeaponDeployed isWeaponRested itemCargo items itemsWithMagazines join joinAs joinAsSilent joinSilent joinString kbAddDatabase kbAddDatabaseTargets kbAddTopic kbHasTopic kbReact kbRemoveTopic kbTell kbWasSaid keyImage keyName knowsAbout land landAt landResult language laserTarget lbAdd lbClear lbColor lbColorRight lbCurSel lbData lbDelete lbIsSelected lbPicture lbPictureRight lbSelection lbSetColor lbSetColorRight lbSetCurSel lbSetData lbSetPicture lbSetPictureColor lbSetPictureColorDisabled lbSetPictureColorSelected lbSetPictureRight lbSetPictureRightColor lbSetPictureRightColorDisabled lbSetPictureRightColorSelected lbSetSelectColor lbSetSelectColorRight lbSetSelected lbSetText lbSetTextRight lbSetTooltip lbSetValue lbSize lbSort lbSortByValue lbText lbTextRight lbValue leader leaderboardDeInit leaderboardGetRows leaderboardInit leaderboardRequestRowsFriends leaderboardsRequestUploadScore leaderboardsRequestUploadScoreKeepBest leaderboardState leaveVehicle libraryCredits libraryDisclaimers lifeState lightAttachObject lightDetachObject lightIsOn lightnings limitSpeed linearConversion lineIntersects lineIntersectsObjs lineIntersectsSurfaces lineIntersectsWith linkItem list listObjects listRemoteTargets listVehicleSensors ln lnbAddArray lnbAddColumn lnbAddRow lnbClear lnbColor lnbCurSelRow lnbData lnbDeleteColumn lnbDeleteRow lnbGetColumnsPosition lnbPicture lnbSetColor lnbSetColumnsPos lnbSetCurSelRow lnbSetData lnbSetPicture lnbSetText lnbSetValue lnbSize lnbSort lnbSortByValue lnbText lnbValue load loadAbs loadBackpack loadFile loadGame loadIdentity loadMagazine loadOverlay loadStatus loadUniform loadVest local localize locationPosition lock lockCameraTo lockCargo lockDriver locked lockedCargo lockedDriver lockedTurret lockIdentity lockTurret lockWP log logEntities logNetwork logNetworkTerminate lookAt lookAtPos magazineCargo magazines magazinesAllTurrets magazinesAmmo magazinesAmmoCargo magazinesAmmoFull magazinesDetail magazinesDetailBackpack magazinesDetailUniform magazinesDetailVest magazinesTurret magazineTurretAmmo mapAnimAdd mapAnimClear mapAnimCommit mapAnimDone mapCenterOnCamera mapGridPosition markAsFinishedOnSteam markerAlpha markerBrush markerColor markerDir markerPos markerShape markerSize markerText markerType max members menuAction menuAdd menuChecked menuClear menuCollapse menuData menuDelete menuEnable menuEnabled menuExpand menuHover menuPicture menuSetAction menuSetCheck menuSetData menuSetPicture menuSetValue menuShortcut menuShortcutText menuSize menuSort menuText menuURL menuValue min mineActive mineDetectedBy missionConfigFile missionDifficulty missionName missionNamespace missionStart missionVersion mod modelToWorld modelToWorldVisual modelToWorldVisualWorld modelToWorldWorld modParams moonIntensity moonPhase morale move move3DENCamera moveInAny moveInCargo moveInCommander moveInDriver moveInGunner moveInTurret moveObjectToEnd moveOut moveTime moveTo moveToCompleted moveToFailed musicVolume name nameSound nearEntities nearestBuilding nearestLocation nearestLocations nearestLocationWithDubbing nearestObject nearestObjects nearestTerrainObjects nearObjects nearObjectsReady nearRoads nearSupplies nearTargets needReload netId netObjNull newOverlay nextMenuItemIndex nextWeatherChange nMenuItems not numberOfEnginesRTD numberToDate objectCurators objectFromNetId objectParent objStatus onBriefingGroup onBriefingNotes onBriefingPlan onBriefingTeamSwitch onCommandModeChanged onDoubleClick onEachFrame onGroupIconClick onGroupIconOverEnter onGroupIconOverLeave onHCGroupSelectionChanged onMapSingleClick onPlayerConnected onPlayerDisconnected onPreloadFinished onPreloadStarted onShowNewObject onTeamSwitch openCuratorInterface openDLCPage openMap openSteamApp openYoutubeVideo or orderGetIn overcast overcastForecast owner param params parseNumber parseSimpleArray parseText parsingNamespace particlesQuality pickWeaponPool pitch pixelGrid pixelGridBase pixelGridNoUIScale pixelH pixelW playableSlotsNumber playableUnits playAction playActionNow player playerRespawnTime playerSide playersNumber playGesture playMission playMove playMoveNow playMusic playScriptedMission playSound playSound3D position positionCameraToWorld posScreenToWorld posWorldToScreen ppEffectAdjust ppEffectCommit ppEffectCommitted ppEffectCreate ppEffectDestroy ppEffectEnable ppEffectEnabled ppEffectForceInNVG precision preloadCamera preloadObject preloadSound preloadTitleObj preloadTitleRsc preprocessFile preprocessFileLineNumbers primaryWeapon primaryWeaponItems primaryWeaponMagazine priority processDiaryLink productVersion profileName profileNamespace profileNameSteam progressLoadingScreen progressPosition progressSetPosition publicVariable publicVariableClient publicVariableServer pushBack pushBackUnique putWeaponPool queryItemsPool queryMagazinePool queryWeaponPool rad radioChannelAdd radioChannelCreate radioChannelRemove radioChannelSetCallSign radioChannelSetLabel radioVolume rain rainbow random rank rankId rating rectangular registeredTasks registerTask reload reloadEnabled remoteControl remoteExec remoteExecCall remoteExecutedOwner remove3DENConnection remove3DENEventHandler remove3DENLayer removeAction removeAll3DENEventHandlers removeAllActions removeAllAssignedItems removeAllContainers removeAllCuratorAddons removeAllCuratorCameraAreas removeAllCuratorEditingAreas removeAllEventHandlers removeAllHandgunItems removeAllItems removeAllItemsWithMagazines removeAllMissionEventHandlers removeAllMPEventHandlers removeAllMusicEventHandlers removeAllOwnedMines removeAllPrimaryWeaponItems removeAllWeapons removeBackpack removeBackpackGlobal removeCuratorAddons removeCuratorCameraArea removeCuratorEditableObjects removeCuratorEditingArea removeDrawIcon removeDrawLinks removeEventHandler removeFromRemainsCollector removeGoggles removeGroupIcon removeHandgunItem removeHeadgear removeItem removeItemFromBackpack removeItemFromUniform removeItemFromVest removeItems removeMagazine removeMagazineGlobal removeMagazines removeMagazinesTurret removeMagazineTurret removeMenuItem removeMissionEventHandler removeMPEventHandler removeMusicEventHandler removeOwnedMine removePrimaryWeaponItem removeSecondaryWeaponItem removeSimpleTask removeSwitchableUnit removeTeamMember removeUniform removeVest removeWeapon removeWeaponAttachmentCargo removeWeaponCargo removeWeaponGlobal removeWeaponTurret reportRemoteTarget requiredVersion resetCamShake resetSubgroupDirection resize resources respawnVehicle restartEditorCamera reveal revealMine reverse reversedMouseY roadAt roadsConnectedTo roleDescription ropeAttachedObjects ropeAttachedTo ropeAttachEnabled ropeAttachTo ropeCreate ropeCut ropeDestroy ropeDetach ropeEndPosition ropeLength ropes ropeUnwind ropeUnwound rotorsForcesRTD rotorsRpmRTD round runInitScript safeZoneH safeZoneW safeZoneWAbs safeZoneX safeZoneXAbs safeZoneY save3DENInventory saveGame saveIdentity saveJoysticks saveOverlay saveProfileNamespace saveStatus saveVar savingEnabled say say2D say3D scopeName score scoreSide screenshot screenToWorld scriptDone scriptName scudState secondaryWeapon secondaryWeaponItems secondaryWeaponMagazine select selectBestPlaces selectDiarySubject selectedEditorObjects selectEditorObject selectionNames selectionPosition selectLeader selectMax selectMin selectNoPlayer selectPlayer selectRandom selectRandomWeighted selectWeapon selectWeaponTurret sendAUMessage sendSimpleCommand sendTask sendTaskResult sendUDPMessage serverCommand serverCommandAvailable serverCommandExecutable serverName serverTime set set3DENAttribute set3DENAttributes set3DENGrid set3DENIconsVisible set3DENLayer set3DENLinesVisible set3DENLogicType set3DENMissionAttribute set3DENMissionAttributes set3DENModelsVisible set3DENObjectType set3DENSelected setAccTime setActualCollectiveRTD setAirplaneThrottle setAirportSide setAmmo setAmmoCargo setAmmoOnPylon setAnimSpeedCoef setAperture setApertureNew setArmoryPoints setAttributes setAutonomous setBehaviour setBleedingRemaining setBrakesRTD setCameraInterest setCamShakeDefParams setCamShakeParams setCamUseTI setCaptive setCenterOfMass setCollisionLight setCombatMode setCompassOscillation setConvoySeparation setCuratorCameraAreaCeiling setCuratorCoef setCuratorEditingAreaType setCuratorWaypointCost setCurrentChannel setCurrentTask setCurrentWaypoint setCustomAimCoef setCustomWeightRTD setDamage setDammage setDate setDebriefingText setDefaultCamera setDestination setDetailMapBlendPars setDir setDirection setDrawIcon setDriveOnPath setDropInterval setDynamicSimulationDistance setDynamicSimulationDistanceCoef setEditorMode setEditorObjectScope setEffectCondition setEngineRPMRTD setFace setFaceAnimation setFatigue setFeatureType setFlagAnimationPhase setFlagOwner setFlagSide setFlagTexture setFog setFormation setFormationTask setFormDir setFriend setFromEditor setFSMVariable setFuel setFuelCargo setGroupIcon setGroupIconParams setGroupIconsSelectable setGroupIconsVisible setGroupId setGroupIdGlobal setGroupOwner setGusts setHideBehind setHit setHitIndex setHitPointDamage setHorizonParallaxCoef setHUDMovementLevels setIdentity setImportance setInfoPanel setLeader setLightAmbient setLightAttenuation setLightBrightness setLightColor setLightDayLight setLightFlareMaxDistance setLightFlareSize setLightIntensity setLightnings setLightUseFlare setLocalWindParams setMagazineTurretAmmo setMarkerAlpha setMarkerAlphaLocal setMarkerBrush setMarkerBrushLocal setMarkerColor setMarkerColorLocal setMarkerDir setMarkerDirLocal setMarkerPos setMarkerPosLocal setMarkerShape setMarkerShapeLocal setMarkerSize setMarkerSizeLocal setMarkerText setMarkerTextLocal setMarkerType setMarkerTypeLocal setMass setMimic setMousePosition setMusicEffect setMusicEventHandler setName setNameSound setObjectArguments setObjectMaterial setObjectMaterialGlobal setObjectProxy setObjectTexture setObjectTextureGlobal setObjectViewDistance setOvercast setOwner setOxygenRemaining setParticleCircle setParticleClass setParticleFire setParticleParams setParticleRandom setPilotCameraDirection setPilotCameraRotation setPilotCameraTarget setPilotLight setPiPEffect setPitch setPlateNumber setPlayable setPlayerRespawnTime setPos setPosASL setPosASL2 setPosASLW setPosATL setPosition setPosWorld setPylonLoadOut setPylonsPriority setRadioMsg setRain setRainbow setRandomLip setRank setRectangular setRepairCargo setRotorBrakeRTD setShadowDistance setShotParents setSide setSimpleTaskAlwaysVisible setSimpleTaskCustomData setSimpleTaskDescription setSimpleTaskDestination setSimpleTaskTarget setSimpleTaskType setSimulWeatherLayers setSize setSkill setSlingLoad setSoundEffect setSpeaker setSpeech setSpeedMode setStamina setStaminaScheme setStatValue setSuppression setSystemOfUnits setTargetAge setTaskMarkerOffset setTaskResult setTaskState setTerrainGrid setText setTimeMultiplier setTitleEffect setTrafficDensity setTrafficDistance setTrafficGap setTrafficSpeed setTriggerActivation setTriggerArea setTriggerStatements setTriggerText setTriggerTimeout setTriggerType setType setUnconscious setUnitAbility setUnitLoadout setUnitPos setUnitPosWeak setUnitRank setUnitRecoilCoefficient setUnitTrait setUnloadInCombat setUserActionText setUserMFDText setUserMFDvalue setVariable setVectorDir setVectorDirAndUp setVectorUp setVehicleAmmo setVehicleAmmoDef setVehicleArmor setVehicleCargo setVehicleId setVehicleLock setVehiclePosition setVehicleRadar setVehicleReceiveRemoteTargets setVehicleReportOwnPosition setVehicleReportRemoteTargets setVehicleTIPars setVehicleVarName setVelocity setVelocityModelSpace setVelocityTransformation setViewDistance setVisibleIfTreeCollapsed setWantedRPMRTD setWaves setWaypointBehaviour setWaypointCombatMode setWaypointCompletionRadius setWaypointDescription setWaypointForceBehaviour setWaypointFormation setWaypointHousePosition setWaypointLoiterRadius setWaypointLoiterType setWaypointName setWaypointPosition setWaypointScript setWaypointSpeed setWaypointStatements setWaypointTimeout setWaypointType setWaypointVisible setWeaponReloadingTime setWind setWindDir setWindForce setWindStr setWingForceScaleRTD setWPPos show3DIcons showChat showCinemaBorder showCommandingMenu showCompass showCuratorCompass showGPS showHUD showLegend showMap shownArtilleryComputer shownChat shownCompass shownCuratorCompass showNewEditorObject shownGPS shownHUD shownMap shownPad shownRadio shownScoretable shownUAVFeed shownWarrant shownWatch showPad showRadio showScoretable showSubtitles showUAVFeed showWarrant showWatch showWaypoint showWaypoints side sideChat sideEnemy sideFriendly sideRadio simpleTasks simulationEnabled simulCloudDensity simulCloudOcclusion simulInClouds simulWeatherSync sin size sizeOf skill skillFinal skipTime sleep sliderPosition sliderRange sliderSetPosition sliderSetRange sliderSetSpeed sliderSpeed slingLoadAssistantShown soldierMagazines someAmmo sort soundVolume spawn speaker speed speedMode splitString sqrt squadParams stance startLoadingScreen step stop stopEngineRTD stopped str sunOrMoon supportInfo suppressFor surfaceIsWater surfaceNormal surfaceType swimInDepth switchableUnits switchAction switchCamera switchGesture switchLight switchMove synchronizedObjects synchronizedTriggers synchronizedWaypoints synchronizeObjectsAdd synchronizeObjectsRemove synchronizeTrigger synchronizeWaypoint systemChat systemOfUnits tan targetKnowledge targets targetsAggregate targetsQuery taskAlwaysVisible taskChildren taskCompleted taskCustomData taskDescription taskDestination taskHint taskMarkerOffset taskParent taskResult taskState taskType teamMember teamName teams teamSwitch teamSwitchEnabled teamType terminate terrainIntersect terrainIntersectASL terrainIntersectAtASL text textLog textLogFormat tg time timeMultiplier titleCut titleFadeOut titleObj titleRsc titleText toArray toFixed toLower toString toUpper triggerActivated triggerActivation triggerArea triggerAttachedVehicle triggerAttachObject triggerAttachVehicle triggerDynamicSimulation triggerStatements triggerText triggerTimeout triggerTimeoutCurrent triggerType turretLocal turretOwner turretUnit tvAdd tvClear tvCollapse tvCollapseAll tvCount tvCurSel tvData tvDelete tvExpand tvExpandAll tvPicture tvSetColor tvSetCurSel tvSetData tvSetPicture tvSetPictureColor tvSetPictureColorDisabled tvSetPictureColorSelected tvSetPictureRight tvSetPictureRightColor tvSetPictureRightColorDisabled tvSetPictureRightColorSelected tvSetText tvSetTooltip tvSetValue tvSort tvSortByValue tvText tvTooltip tvValue type typeName typeOf UAVControl uiNamespace uiSleep unassignCurator unassignItem unassignTeam unassignVehicle underwater uniform uniformContainer uniformItems uniformMagazines unitAddons unitAimPosition unitAimPositionVisual unitBackpack unitIsUAV unitPos unitReady unitRecoilCoefficient units unitsBelowHeight unlinkItem unlockAchievement unregisterTask updateDrawIcon updateMenuItem updateObjectTree useAISteeringComponent useAudioTimeForMoves userInputDisabled vectorAdd vectorCos vectorCrossProduct vectorDiff vectorDir vectorDirVisual vectorDistance vectorDistanceSqr vectorDotProduct vectorFromTo vectorMagnitude vectorMagnitudeSqr vectorModelToWorld vectorModelToWorldVisual vectorMultiply vectorNormalized vectorUp vectorUpVisual vectorWorldToModel vectorWorldToModelVisual vehicle vehicleCargoEnabled vehicleChat vehicleRadio vehicleReceiveRemoteTargets vehicleReportOwnPosition vehicleReportRemoteTargets vehicles vehicleVarName velocity velocityModelSpace verifySignature vest vestContainer vestItems vestMagazines viewDistance visibleCompass visibleGPS visibleMap visiblePosition visiblePositionASL visibleScoretable visibleWatch waves waypointAttachedObject waypointAttachedVehicle waypointAttachObject waypointAttachVehicle waypointBehaviour waypointCombatMode waypointCompletionRadius waypointDescription waypointForceBehaviour waypointFormation waypointHousePosition waypointLoiterRadius waypointLoiterType waypointName waypointPosition waypoints waypointScript waypointsEnabledUAV waypointShow waypointSpeed waypointStatements waypointTimeout waypointTimeoutCurrent waypointType waypointVisible weaponAccessories weaponAccessoriesCargo weaponCargo weaponDirection weaponInertia weaponLowered weapons weaponsItems weaponsItemsCargo weaponState weaponsTurret weightRTD WFSideText wind ",
 literal:"blufor civilian configNull controlNull displayNull east endl false grpNull independent lineBreak locationNull nil objNull opfor pi resistance scriptNull sideAmbientLife sideEmpty sideLogic sideUnknown taskNull teamMemberNull true west"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.NUMBER_MODE,{className:"variable",begin:/\b_+[a-zA-Z_]\w*/},{className:"title",begin:/[a-zA-Z][a-zA-Z0-9]+_fnc_\w*/},{className:"string",variants:[{begin:'"',end:'"',contains:[{begin:'""',relevance:0}]},
-{begin:"'",end:"'",contains:[{begin:"''",relevance:0}]}]},b.preprocessor],illegal:/#|^\$ /}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment with",
-end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
-literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,
+{begin:"'",end:"'",contains:[{begin:"''",relevance:0}]}]},b.preprocessor],illegal:/#|^\$ /}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",
+end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
+literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,
 a.C_BLOCK_COMMENT_MODE,b,a.HASH_COMMENT_MODE]},a.C_BLOCK_COMMENT_MODE,b,a.HASH_COMMENT_MODE]}});b.registerLanguage("stan",function(a){return{contains:[a.HASH_COMMENT_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{begin:a.UNDERSCORE_IDENT_RE,lexemes:a.UNDERSCORE_IDENT_RE,keywords:{name:"for in while repeat until if then else",symbol:"bernoulli bernoulli_logit binomial binomial_logit beta_binomial hypergeometric categorical categorical_logit ordered_logistic neg_binomial neg_binomial_2 neg_binomial_2_log poisson poisson_log multinomial normal exp_mod_normal skew_normal student_t cauchy double_exponential logistic gumbel lognormal chi_square inv_chi_square scaled_inv_chi_square exponential inv_gamma weibull frechet rayleigh wiener pareto pareto_type_2 von_mises uniform multi_normal multi_normal_prec multi_normal_cholesky multi_gp multi_gp_cholesky multi_student_t gaussian_dlm_obs dirichlet lkj_corr lkj_corr_cholesky wishart inv_wishart",
 "selector-tag":"int real vector simplex unit_vector ordered positive_ordered row_vector matrix cholesky_factor_corr cholesky_factor_cov corr_matrix cov_matrix",title:"functions model data parameters quantities transformed generated",literal:"true false"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",
 relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0}]}});b.registerLanguage("stata",function(a){return{aliases:["do","ado"],case_insensitive:!0,keywords:"if else in foreach for forv forva forval forvalu forvalue forvalues by bys bysort xi quietly qui capture about ac ac_7 acprplot acprplot_7 adjust ado adopath adoupdate alpha ameans an ano anov anova anova_estat anova_terms anovadef aorder ap app appe appen append arch arch_dr arch_estat arch_p archlm areg areg_p args arima arima_dr arima_estat arima_p as asmprobit asmprobit_estat asmprobit_lf asmprobit_mfx__dlg asmprobit_p ass asse asser assert avplot avplot_7 avplots avplots_7 bcskew0 bgodfrey binreg bip0_lf biplot bipp_lf bipr_lf bipr_p biprobit bitest bitesti bitowt blogit bmemsize boot bootsamp bootstrap bootstrap_8 boxco_l boxco_p boxcox boxcox_6 boxcox_p bprobit br break brier bro brow brows browse brr brrstat bs bs_7 bsampl_w bsample bsample_7 bsqreg bstat bstat_7 bstat_8 bstrap bstrap_7 ca ca_estat ca_p cabiplot camat canon canon_8 canon_8_p canon_estat canon_p cap caprojection capt captu captur capture cat cc cchart cchart_7 cci cd censobs_table centile cf char chdir checkdlgfiles checkestimationsample checkhlpfiles checksum chelp ci cii cl class classutil clear cli clis clist clo clog clog_lf clog_p clogi clogi_sw clogit clogit_lf clogit_p clogitp clogl_sw cloglog clonevar clslistarray cluster cluster_measures cluster_stop cluster_tree cluster_tree_8 clustermat cmdlog cnr cnre cnreg cnreg_p cnreg_sw cnsreg codebook collaps4 collapse colormult_nb colormult_nw compare compress conf confi confir confirm conren cons const constr constra constrai constrain constraint continue contract copy copyright copysource cor corc corr corr2data corr_anti corr_kmo corr_smc corre correl correla correlat correlate corrgram cou coun count cox cox_p cox_sw coxbase coxhaz coxvar cprplot cprplot_7 crc cret cretu cretur creturn cross cs cscript cscript_log csi ct ct_is ctset ctst_5 ctst_st cttost cumsp cumsp_7 cumul cusum cusum_7 cutil d|0 datasig datasign datasigna datasignat datasignatu datasignatur datasignature datetof db dbeta de dec deco decod decode deff des desc descr descri describ describe destring dfbeta dfgls dfuller di di_g dir dirstats dis discard disp disp_res disp_s displ displa display distinct do doe doed doedi doedit dotplot dotplot_7 dprobit drawnorm drop ds ds_util dstdize duplicates durbina dwstat dydx e|0 ed edi edit egen eivreg emdef en enc enco encod encode eq erase ereg ereg_lf ereg_p ereg_sw ereghet ereghet_glf ereghet_glf_sh ereghet_gp ereghet_ilf ereghet_ilf_sh ereghet_ip eret eretu eretur ereturn err erro error est est_cfexist est_cfname est_clickable est_expand est_hold est_table est_unhold est_unholdok estat estat_default estat_summ estat_vce_only esti estimates etodow etof etomdy ex exi exit expand expandcl fac fact facto factor factor_estat factor_p factor_pca_rotated factor_rotate factormat fcast fcast_compute fcast_graph fdades fdadesc fdadescr fdadescri fdadescrib fdadescribe fdasav fdasave fdause fh_st file open file read file close file filefilter fillin find_hlp_file findfile findit findit_7 fit fl fli flis flist for5_0 form forma format fpredict frac_154 frac_adj frac_chk frac_cox frac_ddp frac_dis frac_dv frac_in frac_mun frac_pp frac_pq frac_pv frac_wgt frac_xo fracgen fracplot fracplot_7 fracpoly fracpred fron_ex fron_hn fron_p fron_tn fron_tn2 frontier ftodate ftoe ftomdy ftowdate g|0 gamhet_glf gamhet_gp gamhet_ilf gamhet_ip gamma gamma_d2 gamma_p gamma_sw gammahet gdi_hexagon gdi_spokes ge gen gene gener genera generat generate genrank genstd genvmean gettoken gl gladder gladder_7 glim_l01 glim_l02 glim_l03 glim_l04 glim_l05 glim_l06 glim_l07 glim_l08 glim_l09 glim_l10 glim_l11 glim_l12 glim_lf glim_mu glim_nw1 glim_nw2 glim_nw3 glim_p glim_v1 glim_v2 glim_v3 glim_v4 glim_v5 glim_v6 glim_v7 glm glm_6 glm_p glm_sw glmpred glo glob globa global glogit glogit_8 glogit_p gmeans gnbre_lf gnbreg gnbreg_5 gnbreg_p gomp_lf gompe_sw gomper_p gompertz gompertzhet gomphet_glf gomphet_glf_sh gomphet_gp gomphet_ilf gomphet_ilf_sh gomphet_ip gphdot gphpen gphprint gprefs gprobi_p gprobit gprobit_8 gr gr7 gr_copy gr_current gr_db gr_describe gr_dir gr_draw gr_draw_replay gr_drop gr_edit gr_editviewopts gr_example gr_example2 gr_export gr_print gr_qscheme gr_query gr_read gr_rename gr_replay gr_save gr_set gr_setscheme gr_table gr_undo gr_use graph graph7 grebar greigen greigen_7 greigen_8 grmeanby grmeanby_7 gs_fileinfo gs_filetype gs_graphinfo gs_stat gsort gwood h|0 hadimvo hareg hausman haver he heck_d2 heckma_p heckman heckp_lf heckpr_p heckprob hel help hereg hetpr_lf hetpr_p hetprob hettest hexdump hilite hist hist_7 histogram hlogit hlu hmeans hotel hotelling hprobit hreg hsearch icd9 icd9_ff icd9p iis impute imtest inbase include inf infi infil infile infix inp inpu input ins insheet insp inspe inspec inspect integ inten intreg intreg_7 intreg_p intrg2_ll intrg_ll intrg_ll2 ipolate iqreg ir irf irf_create irfm iri is_svy is_svysum isid istdize ivprob_1_lf ivprob_lf ivprobit ivprobit_p ivreg ivreg_footnote ivtob_1_lf ivtob_lf ivtobit ivtobit_p jackknife jacknife jknife jknife_6 jknife_8 jkstat joinby kalarma1 kap kap_3 kapmeier kappa kapwgt kdensity kdensity_7 keep ksm ksmirnov ktau kwallis l|0 la lab labe label labelbook ladder levels levelsof leverage lfit lfit_p li lincom line linktest lis list lloghet_glf lloghet_glf_sh lloghet_gp lloghet_ilf lloghet_ilf_sh lloghet_ip llogi_sw llogis_p llogist llogistic llogistichet lnorm_lf lnorm_sw lnorma_p lnormal lnormalhet lnormhet_glf lnormhet_glf_sh lnormhet_gp lnormhet_ilf lnormhet_ilf_sh lnormhet_ip lnskew0 loadingplot loc loca local log logi logis_lf logistic logistic_p logit logit_estat logit_p loglogs logrank loneway lookfor lookup lowess lowess_7 lpredict lrecomp lroc lroc_7 lrtest ls lsens lsens_7 lsens_x lstat ltable ltable_7 ltriang lv lvr2plot lvr2plot_7 m|0 ma mac macr macro makecns man manova manova_estat manova_p manovatest mantel mark markin markout marksample mat mat_capp mat_order mat_put_rr mat_rapp mata mata_clear mata_describe mata_drop mata_matdescribe mata_matsave mata_matuse mata_memory mata_mlib mata_mosave mata_rename mata_which matalabel matcproc matlist matname matr matri matrix matrix_input__dlg matstrik mcc mcci md0_ md1_ md1debug_ md2_ md2debug_ mds mds_estat mds_p mdsconfig mdslong mdsmat mdsshepard mdytoe mdytof me_derd mean means median memory memsize meqparse mer merg merge mfp mfx mhelp mhodds minbound mixed_ll mixed_ll_reparm mkassert mkdir mkmat mkspline ml ml_5 ml_adjs ml_bhhhs ml_c_d ml_check ml_clear ml_cnt ml_debug ml_defd ml_e0 ml_e0_bfgs ml_e0_cycle ml_e0_dfp ml_e0i ml_e1 ml_e1_bfgs ml_e1_bhhh ml_e1_cycle ml_e1_dfp ml_e2 ml_e2_cycle ml_ebfg0 ml_ebfr0 ml_ebfr1 ml_ebh0q ml_ebhh0 ml_ebhr0 ml_ebr0i ml_ecr0i ml_edfp0 ml_edfr0 ml_edfr1 ml_edr0i ml_eds ml_eer0i ml_egr0i ml_elf ml_elf_bfgs ml_elf_bhhh ml_elf_cycle ml_elf_dfp ml_elfi ml_elfs ml_enr0i ml_enrr0 ml_erdu0 ml_erdu0_bfgs ml_erdu0_bhhh ml_erdu0_bhhhq ml_erdu0_cycle ml_erdu0_dfp ml_erdu0_nrbfgs ml_exde ml_footnote ml_geqnr ml_grad0 ml_graph ml_hbhhh ml_hd0 ml_hold ml_init ml_inv ml_log ml_max ml_mlout ml_mlout_8 ml_model ml_nb0 ml_opt ml_p ml_plot ml_query ml_rdgrd ml_repor ml_s_e ml_score ml_searc ml_technique ml_unhold mleval mlf_ mlmatbysum mlmatsum mlog mlogi mlogit mlogit_footnote mlogit_p mlopts mlsum mlvecsum mnl0_ mor more mov move mprobit mprobit_lf mprobit_p mrdu0_ mrdu1_ mvdecode mvencode mvreg mvreg_estat n|0 nbreg nbreg_al nbreg_lf nbreg_p nbreg_sw nestreg net newey newey_7 newey_p news nl nl_7 nl_9 nl_9_p nl_p nl_p_7 nlcom nlcom_p nlexp2 nlexp2_7 nlexp2a nlexp2a_7 nlexp3 nlexp3_7 nlgom3 nlgom3_7 nlgom4 nlgom4_7 nlinit nllog3 nllog3_7 nllog4 nllog4_7 nlog_rd nlogit nlogit_p nlogitgen nlogittree nlpred no nobreak noi nois noisi noisil noisily note notes notes_dlg nptrend numlabel numlist odbc old_ver olo olog ologi ologi_sw ologit ologit_p ologitp on one onew onewa oneway op_colnm op_comp op_diff op_inv op_str opr opro oprob oprob_sw oprobi oprobi_p oprobit oprobitp opts_exclusive order orthog orthpoly ou out outf outfi outfil outfile outs outsh outshe outshee outsheet ovtest pac pac_7 palette parse parse_dissim pause pca pca_8 pca_display pca_estat pca_p pca_rotate pcamat pchart pchart_7 pchi pchi_7 pcorr pctile pentium pergram pergram_7 permute permute_8 personal peto_st pkcollapse pkcross pkequiv pkexamine pkexamine_7 pkshape pksumm pksumm_7 pl plo plot plugin pnorm pnorm_7 poisgof poiss_lf poiss_sw poisso_p poisson poisson_estat post postclose postfile postutil pperron pr prais prais_e prais_e2 prais_p predict predictnl preserve print pro prob probi probit probit_estat probit_p proc_time procoverlay procrustes procrustes_estat procrustes_p profiler prog progr progra program prop proportion prtest prtesti pwcorr pwd q\\s qby qbys qchi qchi_7 qladder qladder_7 qnorm qnorm_7 qqplot qqplot_7 qreg qreg_c qreg_p qreg_sw qu quadchk quantile quantile_7 que quer query range ranksum ratio rchart rchart_7 rcof recast reclink recode reg reg3 reg3_p regdw regr regre regre_p2 regres regres_p regress regress_estat regriv_p remap ren rena renam rename renpfix repeat replace report reshape restore ret retu retur return rm rmdir robvar roccomp roccomp_7 roccomp_8 rocf_lf rocfit rocfit_8 rocgold rocplot rocplot_7 roctab roctab_7 rolling rologit rologit_p rot rota rotat rotate rotatemat rreg rreg_p ru run runtest rvfplot rvfplot_7 rvpplot rvpplot_7 sa safesum sample sampsi sav save savedresults saveold sc sca scal scala scalar scatter scm_mine sco scob_lf scob_p scobi_sw scobit scor score scoreplot scoreplot_help scree screeplot screeplot_help sdtest sdtesti se search separate seperate serrbar serrbar_7 serset set set_defaults sfrancia sh she shel shell shewhart shewhart_7 signestimationsample signrank signtest simul simul_7 simulate simulate_8 sktest sleep slogit slogit_d2 slogit_p smooth snapspan so sor sort spearman spikeplot spikeplot_7 spikeplt spline_x split sqreg sqreg_p sret sretu sretur sreturn ssc st st_ct st_hc st_hcd st_hcd_sh st_is st_issys st_note st_promo st_set st_show st_smpl st_subid stack statsby statsby_8 stbase stci stci_7 stcox stcox_estat stcox_fr stcox_fr_ll stcox_p stcox_sw stcoxkm stcoxkm_7 stcstat stcurv stcurve stcurve_7 stdes stem stepwise stereg stfill stgen stir stjoin stmc stmh stphplot stphplot_7 stphtest stphtest_7 stptime strate strate_7 streg streg_sw streset sts sts_7 stset stsplit stsum sttocc sttoct stvary stweib su suest suest_8 sum summ summa summar summari summariz summarize sunflower sureg survcurv survsum svar svar_p svmat svy svy_disp svy_dreg svy_est svy_est_7 svy_estat svy_get svy_gnbreg_p svy_head svy_header svy_heckman_p svy_heckprob_p svy_intreg_p svy_ivreg_p svy_logistic_p svy_logit_p svy_mlogit_p svy_nbreg_p svy_ologit_p svy_oprobit_p svy_poisson_p svy_probit_p svy_regress_p svy_sub svy_sub_7 svy_x svy_x_7 svy_x_p svydes svydes_8 svygen svygnbreg svyheckman svyheckprob svyintreg svyintreg_7 svyintrg svyivreg svylc svylog_p svylogit svymarkout svymarkout_8 svymean svymlog svymlogit svynbreg svyolog svyologit svyoprob svyoprobit svyopts svypois svypois_7 svypoisson svyprobit svyprobt svyprop svyprop_7 svyratio svyreg svyreg_p svyregress svyset svyset_7 svyset_8 svytab svytab_7 svytest svytotal sw sw_8 swcnreg swcox swereg swilk swlogis swlogit swologit swoprbt swpois swprobit swqreg swtobit swweib symmetry symmi symplot symplot_7 syntax sysdescribe sysdir sysuse szroeter ta tab tab1 tab2 tab_or tabd tabdi tabdis tabdisp tabi table tabodds tabodds_7 tabstat tabu tabul tabula tabulat tabulate te tempfile tempname tempvar tes test testnl testparm teststd tetrachoric time_it timer tis tob tobi tobit tobit_p tobit_sw token tokeni tokeniz tokenize tostring total translate translator transmap treat_ll treatr_p treatreg trim trnb_cons trnb_mean trpoiss_d2 trunc_ll truncr_p truncreg tsappend tset tsfill tsline tsline_ex tsreport tsrevar tsrline tsset tssmooth tsunab ttest ttesti tut_chk tut_wait tutorial tw tware_st two twoway twoway__fpfit_serset twoway__function_gen twoway__histogram_gen twoway__ipoint_serset twoway__ipoints_serset twoway__kdensity_gen twoway__lfit_serset twoway__normgen_gen twoway__pci_serset twoway__qfit_serset twoway__scatteri_serset twoway__sunflower_gen twoway_ksm_serset ty typ type typeof u|0 unab unabbrev unabcmd update us use uselabel var var_mkcompanion var_p varbasic varfcast vargranger varirf varirf_add varirf_cgraph varirf_create varirf_ctable varirf_describe varirf_dir varirf_drop varirf_erase varirf_graph varirf_ograph varirf_rename varirf_set varirf_table varlist varlmar varnorm varsoc varstable varstable_w varstable_w2 varwle vce vec vec_fevd vec_mkphi vec_p vec_p_w vecirf_create veclmar veclmar_w vecnorm vecnorm_w vecrank vecstable verinst vers versi versio version view viewsource vif vwls wdatetof webdescribe webseek webuse weib1_lf weib2_lf weib_lf weib_lf0 weibhet_glf weibhet_glf_sh weibhet_glfa weibhet_glfa_sh weibhet_gp weibhet_ilf weibhet_ilf_sh weibhet_ilfa weibhet_ilfa_sh weibhet_ip weibu_sw weibul_p weibull weibull_c weibull_s weibullhet wh whelp whi which whil while wilc_st wilcoxon win wind windo window winexec wntestb wntestb_7 wntestq xchart xchart_7 xcorr xcorr_7 xi xi_6 xmlsav xmlsave xmluse xpose xsh xshe xshel xshell xt_iis xt_tis xtab_p xtabond xtbin_p xtclog xtcloglog xtcloglog_8 xtcloglog_d2 xtcloglog_pa_p xtcloglog_re_p xtcnt_p xtcorr xtdata xtdes xtfront_p xtfrontier xtgee xtgee_elink xtgee_estat xtgee_makeivar xtgee_p xtgee_plink xtgls xtgls_p xthaus xthausman xtht_p xthtaylor xtile xtint_p xtintreg xtintreg_8 xtintreg_d2 xtintreg_p xtivp_1 xtivp_2 xtivreg xtline xtline_ex xtlogit xtlogit_8 xtlogit_d2 xtlogit_fe_p xtlogit_pa_p xtlogit_re_p xtmixed xtmixed_estat xtmixed_p xtnb_fe xtnb_lf xtnbreg xtnbreg_pa_p xtnbreg_refe_p xtpcse xtpcse_p xtpois xtpoisson xtpoisson_d2 xtpoisson_pa_p xtpoisson_refe_p xtpred xtprobit xtprobit_8 xtprobit_d2 xtprobit_re_p xtps_fe xtps_lf xtps_ren xtps_ren_8 xtrar_p xtrc xtrc_p xtrchh xtrefe_p xtreg xtreg_be xtreg_fe xtreg_ml xtreg_pa_p xtreg_re xtregar xtrere_p xtset xtsf_ll xtsf_llti xtsum xttab xttest0 xttobit xttobit_8 xttobit_p xttrans yx yxview__barlike_draw yxview_area_draw yxview_bar_draw yxview_dot_draw yxview_dropline_draw yxview_function_draw yxview_iarrow_draw yxview_ilabels_draw yxview_normal_draw yxview_pcarrow_draw yxview_pcbarrow_draw yxview_pccapsym_draw yxview_pcscatter_draw yxview_pcspike_draw yxview_rarea_draw yxview_rbar_draw yxview_rbarm_draw yxview_rcap_draw yxview_rcapsym_draw yxview_rconnected_draw yxview_rline_draw yxview_rscatter_draw yxview_rspike_draw yxview_spike_draw yxview_sunflower_draw zap_s zinb zinb_llf zinb_plf zip zip_llf zip_p zip_plf zt_ct_5 zt_hc_5 zt_hcd_5 zt_is_5 zt_iss_5 zt_sho_5 zt_smp_5 ztbase_5 ztcox_5 ztdes_5 ztereg_5 ztfill_5 ztgen_5 ztir_5 ztjoin_5 ztnb ztnb_p ztp ztp_p zts_5 ztset_5 ztspli_5 ztsum_5 zttoct_5 ztvary_5 ztweib_5",
@@ -383,28 +426,29 @@
 returnBegin:!0,contains:[{className:"selector-tag",begin:"\\b[a-zA-Z][a-zA-Z0-9_-]*"}]},{begin:"&?:?:\\b(after|before|first-letter|first-line|active|first-child|focus|hover|lang|link|visited)[\\.\\s\\n\\[\\:,]"},{begin:"@(charset|css|debug|extend|font-face|for|import|include|media|mixin|page|warn|while)\\b"},b,a.CSS_NUMBER_MODE,a.NUMBER_MODE,{className:"function",begin:"^[a-zA-Z][a-zA-Z0-9_-]*\\(.*\\)",illegal:"[\\n]",returnBegin:!0,contains:[{className:"title",begin:"\\b[a-zA-Z][a-zA-Z0-9_-]*"},
 {className:"params",begin:/\(/,end:/\)/,contains:[d,b,a.APOS_STRING_MODE,a.CSS_NUMBER_MODE,a.NUMBER_MODE,a.QUOTE_STRING_MODE]}]},{className:"attribute",begin:"\\b("+"align-content align-items align-self animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function auto backface-visibility background background-attachment background-clip background-color background-image background-origin background-position background-repeat background-size border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-decoration-break box-shadow box-sizing break-after break-before break-inside caption-side clear clip clip-path color column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset cursor direction display empty-cells filter flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float font font-family font-feature-settings font-kerning font-language-override font-size font-size-adjust font-stretch font-style font-variant font-variant-ligatures font-weight height hyphens icon image-orientation image-rendering image-resolution ime-mode inherit initial justify-content left letter-spacing line-height list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top marks mask max-height max-width min-height min-width nav-down nav-index nav-left nav-right nav-up none normal object-fit object-position opacity order orphans outline outline-color outline-offset outline-style outline-width overflow overflow-wrap overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page-break-after page-break-before page-break-inside perspective perspective-origin pointer-events position quotes resize right tab-size table-layout text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-style text-indent text-overflow text-rendering text-shadow text-transform text-underline-position top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi vertical-align visibility white-space widows width word-break word-spacing word-wrap z-index".split(" ").reverse().join("|")+
 ")\\b",starts:{end:/;|$/,contains:[d,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE,a.NUMBER_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/\./,relevance:0}}]}});b.registerLanguage("subunit",function(a){return{case_insensitive:!0,contains:[{className:"string",begin:"\\[\n(multipart)?",end:"\\]\n"},{className:"string",begin:"\\d{4}-\\d{2}-\\d{2}(\\s+)\\d{2}:\\d{2}:\\d{2}.\\d+Z"},{className:"string",begin:"(\\+|-)\\d+"},{className:"keyword",relevance:10,variants:[{begin:"^(test|testing|success|successful|failure|error|skip|xfail|uxsuccess)(:?)\\s+(test)?"},
-{begin:"^progress(:?)(\\s+)?(pop|push)?"},{begin:"^tags:"},{begin:"^time:"}]}]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
+{begin:"^progress(:?)(\\s+)?(pop|push)?"},{begin:"^tags:"},{begin:"^time:"}]}]}});b.registerLanguage("swift",function(a){var b={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
 literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},
-d=a.COMMENT("/\\*","\\*/",{contains:["self"]}),e={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},f={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},g=a.inherit(a.QUOTE_STRING_MODE,{contains:[e,a.BACKSLASH_ESCAPE]});e.contains=[f];return{keywords:b,contains:[g,a.C_LINE_COMMENT_MODE,d,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},f,{className:"function",beginKeywords:"func",end:"{",
-excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",f,g,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},
-{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,d]}]}});b.registerLanguage("taggerscript",function(a){return{contains:[{className:"comment",begin:/\$noop\(/,end:/\)/,contains:[{begin:/\(/,end:/\)/,contains:["self",{begin:/\\./}]}],relevance:10},{className:"keyword",begin:/\$(?!noop)[a-zA-Z][_a-zA-Z0-9]*/,end:/\(/,excludeEnd:!0},{className:"variable",begin:/%[_a-zA-Z0-9:]*/,end:"%"},{className:"symbol",begin:/\\./}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},
-{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},d={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:d.contains,end:b.variants[0].begin},
-{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},a.C_NUMBER_MODE,d]}});b.registerLanguage("tap",function(a){return{case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,
-{className:"meta",variants:[{begin:"^TAP version (\\d+)$"},{begin:"^1\\.\\.(\\d+)$"}]},{begin:"(s+)?---$",end:"\\.\\.\\.$",subLanguage:"yaml",relevance:0},{className:"number",begin:" (\\d+) "},{className:"symbol",variants:[{begin:"^ok"},{begin:"^not ok"}]}]}});b.registerLanguage("tcl",function(a){return{aliases:["tk"],keywords:"after append apply array auto_execok auto_import auto_load auto_mkindex auto_mkindex_old auto_qualify auto_reset bgerror binary break catch cd chan clock close concat continue dde dict encoding eof error eval exec exit expr fblocked fconfigure fcopy file fileevent filename flush for foreach format gets glob global history http if incr info interp join lappend|10 lassign|10 lindex|10 linsert|10 list llength|10 load lrange|10 lrepeat|10 lreplace|10 lreverse|10 lsearch|10 lset|10 lsort|10 mathfunc mathop memory msgcat namespace open package parray pid pkg::create pkg_mkIndex platform platform::shell proc puts pwd read refchan regexp registry regsub|10 rename return safe scan seek set socket source split string subst switch tcl_endOfWord tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord tcl_wordBreakAfter tcl_wordBreakBefore tcltest tclvars tell time tm trace unknown unload unset update uplevel upvar variable vwait while",
+d=a.COMMENT("/\\*","\\*/",{contains:["self"]}),e={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},f={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},g={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};e.contains=[g];return{keywords:b,contains:[f,a.C_LINE_COMMENT_MODE,d,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*[!?]"},{className:"type",
+begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},g,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",g,f,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,
+{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,d]}]}});b.registerLanguage("taggerscript",function(a){return{contains:[{className:"comment",
+begin:/\$noop\(/,end:/\)/,contains:[{begin:/\(/,end:/\)/,contains:["self",{begin:/\\./}]}],relevance:10},{className:"keyword",begin:/\$(?!noop)[a-zA-Z][_a-zA-Z0-9]*/,end:/\(/,excludeEnd:!0},{className:"variable",begin:/%[_a-zA-Z0-9:]*/,end:"%"},{className:"symbol",begin:/\\./}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},d={className:"string",relevance:0,
+variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:d.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",
+begin:"!"+a.UNDERSCORE_IDENT_RE},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},a.C_NUMBER_MODE,d]}});b.registerLanguage("tap",function(a){return{case_insensitive:!0,contains:[a.HASH_COMMENT_MODE,{className:"meta",variants:[{begin:"^TAP version (\\d+)$"},
+{begin:"^1\\.\\.(\\d+)$"}]},{begin:"(s+)?---$",end:"\\.\\.\\.$",subLanguage:"yaml",relevance:0},{className:"number",begin:" (\\d+) "},{className:"symbol",variants:[{begin:"^ok"},{begin:"^not ok"}]}]}});b.registerLanguage("tcl",function(a){return{aliases:["tk"],keywords:"after append apply array auto_execok auto_import auto_load auto_mkindex auto_mkindex_old auto_qualify auto_reset bgerror binary break catch cd chan clock close concat continue dde dict encoding eof error eval exec exit expr fblocked fconfigure fcopy file fileevent filename flush for foreach format gets glob global history http if incr info interp join lappend|10 lassign|10 lindex|10 linsert|10 list llength|10 load lrange|10 lrepeat|10 lreplace|10 lreverse|10 lsearch|10 lset|10 lsort|10 mathfunc mathop memory msgcat namespace open package parray pid pkg::create pkg_mkIndex platform platform::shell proc puts pwd read refchan regexp registry regsub|10 rename return safe scan seek set socket source split string subst switch tcl_endOfWord tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord tcl_wordBreakAfter tcl_wordBreakBefore tcltest tclvars tell time tm trace unknown unload unset update uplevel upvar variable vwait while",
 contains:[a.COMMENT(";[ \\t]*#","$"),a.COMMENT("^[ \\t]*#","$"),{beginKeywords:"proc",end:"[\\{]",excludeEnd:!0,contains:[{className:"title",begin:"[ \\t\\n\\r]+(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"[ \\t\\n\\r]",endsWithParent:!0,excludeEnd:!0}]},{excludeEnd:!0,variants:[{begin:"\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*\\(([a-zA-Z0-9_])*\\)",end:"[^a-zA-Z0-9_\\}\\$]"},{begin:"\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*",end:"(\\))?[^a-zA-Z0-9_\\}\\$]"}]},{className:"string",contains:[a.BACKSLASH_ESCAPE],
-variants:[a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},{className:"number",variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]}]}});b.registerLanguage("tex",function(a){var b={className:"tag",begin:/\\/,relevance:0,contains:[{className:"name",variants:[{begin:/[a-zA-Z\u0430-\u044f\u0410-\u044f]+[*]?/},{begin:/[^a-zA-Z\u0430-\u044f\u0410-\u044f0-9]/}],starts:{endsWithParent:!0,relevance:0,contains:[{className:"string",variants:[{begin:/\[/,end:/\]/},{begin:/\{/,
+variants:[a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},{className:"number",variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]}]}});b.registerLanguage("tex",function(a){var b={className:"tag",begin:/\\/,relevance:0,contains:[{className:"name",variants:[{begin:/[a-zA-Z\u0430-\u044f\u0410-\u042f]+[*]?/},{begin:/[^a-zA-Z\u0430-\u044f\u0410-\u042f0-9]/}],starts:{endsWithParent:!0,relevance:0,contains:[{className:"string",variants:[{begin:/\[/,end:/\]/},{begin:/\{/,
 end:/\}/}]},{begin:/\s*=\s*/,endsWithParent:!0,relevance:0,contains:[{className:"number",begin:/-?\d*\.?\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?/}]}]}}]};return{contains:[b,{className:"formula",contains:[b],relevance:0,variants:[{begin:/\$\$/,end:/\$\$/},{begin:/\$/,end:/\$/}]},a.COMMENT("%","$",{relevance:0})]}});b.registerLanguage("thrift",function(a){return{keywords:{keyword:"namespace const typedef struct enum service exception void oneway set list map required optional",built_in:"bool byte i16 i32 i64 double string binary",
 literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"struct enum service exception",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{begin:"\\b(set|list|map)\\s*<",end:">",keywords:"bool byte i16 i32 i64 double string binary",contains:["self"]}]}});b.registerLanguage("tp",function(a){var b={className:"number",begin:"[1-9][0-9]*",relevance:0},d={className:"symbol",
 begin:":[^\\]]+"};return{keywords:{keyword:"ABORT ACC ADJUST AND AP_LD BREAK CALL CNT COL CONDITION CONFIG DA DB DIV DETECT ELSE END ENDFOR ERR_NUM ERROR_PROG FINE FOR GP GUARD INC IF JMP LINEAR_MAX_SPEED LOCK MOD MONITOR OFFSET Offset OR OVERRIDE PAUSE PREG PTH RT_LD RUN SELECT SKIP Skip TA TB TO TOOL_OFFSET Tool_Offset UF UT UFRAME_NUM UTOOL_NUM UNLOCK WAIT X Y Z W P R STRLEN SUBSTR FINDSTR VOFFSET PROG ATTR MN POS",literal:"ON OFF max_speed LPOS JPOS ENABLE DISABLE START STOP RESET"},contains:[{className:"built_in",
-begin:"(AR|P|PAYLOAD|PR|R|SR|RSR|LBL|VR|UALM|MESSAGE|UTOOL|UFRAME|TIMER|    TIMER_OVERFLOW|JOINT_MAX_SPEED|RESUME_PROG|DIAG_REC)\\[",end:"\\]",contains:["self",b,d]},{className:"built_in",begin:"(AI|AO|DI|DO|F|RI|RO|UI|UO|GI|GO|SI|SO)\\[",end:"\\]",contains:["self",b,a.QUOTE_STRING_MODE,d]},{className:"keyword",begin:"/(PROG|ATTR|MN|POS|END)\\b"},{className:"keyword",begin:"(CALL|RUN|POINT_LOGIC|LBL)\\b"},{className:"keyword",begin:"\\b(ACC|CNT|Skip|Offset|PSPD|RT_LD|AP_LD|Tool_Offset)"},{className:"number",
+begin:"(AR|P|PAYLOAD|PR|R|SR|RSR|LBL|VR|UALM|MESSAGE|UTOOL|UFRAME|TIMER|TIMER_OVERFLOW|JOINT_MAX_SPEED|RESUME_PROG|DIAG_REC)\\[",end:"\\]",contains:["self",b,d]},{className:"built_in",begin:"(AI|AO|DI|DO|F|RI|RO|UI|UO|GI|GO|SI|SO)\\[",end:"\\]",contains:["self",b,a.QUOTE_STRING_MODE,d]},{className:"keyword",begin:"/(PROG|ATTR|MN|POS|END)\\b"},{className:"keyword",begin:"(CALL|RUN|POINT_LOGIC|LBL)\\b"},{className:"keyword",begin:"\\b(ACC|CNT|Skip|Offset|PSPD|RT_LD|AP_LD|Tool_Offset)"},{className:"number",
 begin:"\\d+(sec|msec|mm/sec|cm/min|inch/min|deg/sec|mm|in|cm)?\\b",relevance:0},a.COMMENT("//","[;$]"),a.COMMENT("!","[;$]"),a.COMMENT("--eg:","$"),a.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"'"},a.C_NUMBER_MODE,{className:"variable",begin:"\\$[A-Za-z0-9_]+"}]}});b.registerLanguage("twig",function(a){var b={beginKeywords:"attribute block constant cycle date dump include max min parent random range source template_from_string",keywords:{name:"attribute block constant cycle date dump include max min parent random range source template_from_string"},
 relevance:0,contains:[{className:"params",begin:"\\(",end:"\\)"}]},d={begin:/\|[A-Za-z_]+:?/,keywords:"abs batch capitalize convert_encoding date date_modify default escape first format join json_encode keys last length lower merge nl2br number_format raw replace reverse round slice sort split striptags title trim upper url_encode",contains:[b]},e="autoescape block do embed extends filter flush for if import include macro sandbox set spaceless use verbatim";e=e+" "+e.split(" ").map(function(a){return"end"+
 a}).join(" ");return{aliases:["craftcms"],case_insensitive:!0,subLanguage:"xml",contains:[a.COMMENT(/\{#/,/#}/),{className:"template-tag",begin:/\{%/,end:/%}/,contains:[{className:"name",begin:/\w+/,keywords:e,starts:{endsWithParent:!0,contains:[d,b],relevance:0}}]},{className:"template-variable",begin:/\{\{/,end:/}}/,contains:["self",d,b]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",
-literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"};
-return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a.IDENT_RE},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),
-{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0,contains:["self",{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},
-{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("vala",function(a){return{keywords:{keyword:"char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double bool struct enum string void weak unowned owned async signal static abstract interface override virtual delegate if while do for foreach else switch case break default return try catch public private protected internal using new this get set const stdout stdin stderr var",
+literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"},
+d={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},e={begin:"\\(",end:/\)/,keywords:b,contains:["self",a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.NUMBER_MODE]},f={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,e]};return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,
+{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a.IDENT_RE},
+{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),f],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0,contains:["self",f]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,
+excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0},d,e]}});b.registerLanguage("vala",function(a){return{keywords:{keyword:"char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double bool struct enum string void weak unowned owned async signal static abstract interface override virtual delegate if while do for foreach else switch case break default return try catch public private protected internal using new this get set const stdout stdin stderr var",
 built_in:"DBus GLib CCode Gee Object Gtk Posix",literal:"false true null"},contains:[{className:"class",beginKeywords:"class interface namespace",end:"{",excludeEnd:!0,illegal:"[^,:\\n\\s\\.]",contains:[a.UNDERSCORE_TITLE_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",begin:'"""',end:'"""',relevance:5},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,{className:"meta",begin:"^#",end:"$",relevance:2}]}});b.registerLanguage("vbnet",function(a){return{aliases:["vb"],case_insensitive:!0,
 keywords:{keyword:"addhandler addressof alias and andalso aggregate ansi as assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into is isfalse isnot istrue join key let lib like loop me mid mod module mustinherit mustoverride mybase myclass namespace narrowing new next not notinheritable notoverridable of off on operator option optional or order orelse overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim rem removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly xor",
 built_in:"boolean byte cbool cbyte cchar cdate cdec cdbl char cint clng cobj csbyte cshort csng cstr ctype date decimal directcast double gettype getxmlnamespace iif integer long object sbyte short single string trycast typeof uinteger ulong ushort",literal:"true false nothing"},illegal:"//|{|}|endif|gosub|variant|wend|^\\$ ",contains:[a.inherit(a.QUOTE_STRING_MODE,{contains:[{begin:'""'}]}),a.COMMENT("'","$",{returnBegin:!0,contains:[{className:"doctag",begin:"'''|\x3c!--|--\x3e",contains:[a.PHRASAL_WORDS_MODE]},
@@ -417,16 +461,19 @@
 built_in:"boolean bit character integer time delay_length natural positive string bit_vector file_open_kind file_open_status std_logic std_logic_vector unsigned signed boolean_vector integer_vector std_ulogic std_ulogic_vector unresolved_unsigned u_unsigned unresolved_signed u_signed real_vector time_vector",literal:"false true note warning error failure line text side width"},illegal:"{",contains:[a.C_BLOCK_COMMENT_MODE,a.COMMENT("--","$"),a.QUOTE_STRING_MODE,{className:"number",begin:"\\b(\\d(_|\\d)*#\\w+(\\.\\w+)?#([eE][-+]?\\d(_|\\d)*)?|\\d(_|\\d)*(\\.\\d(_|\\d)*)?([eE][-+]?\\d(_|\\d)*)?)",
 relevance:0},{className:"string",begin:"'(U|X|0|1|Z|W|L|H|-)'",contains:[a.BACKSLASH_ESCAPE]},{className:"symbol",begin:"'[A-Za-z](_?[A-Za-z0-9])*",contains:[a.BACKSLASH_ESCAPE]}]}});b.registerLanguage("vim",function(a){return{lexemes:/[!#@\w]+/,keywords:{keyword:"N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank",
 built_in:"synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv complete_check add getwinposx getqflist getwinposy screencol clearmatches empty extend getcmdpos mzeval garbagecollect setreg ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable shiftwidth max sinh isdirectory synID system inputrestore winline atan visualmode inputlist tabpagewinnr round getregtype mapcheck hasmapto histdel argidx findfile sha256 exists toupper getcmdline taglist string getmatches bufnr strftime winwidth bufexists strtrans tabpagebuflist setcmdpos remote_read printf setloclist getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval resolve libcallnr foldclosedend reverse filter has_key bufname str2float strlen setline getcharmod setbufvar index searchpos shellescape undofile foldclosed setqflist buflisted strchars str2nr virtcol floor remove undotree remote_expr winheight gettabwinvar reltime cursor tabpagenr finddir localtime acos getloclist search tanh matchend rename gettabvar strdisplaywidth type abs py3eval setwinvar tolower wildmenumode log10 spellsuggest bufloaded synconcealed nextnonblank server2client complete settabwinvar executable input wincol setmatches getftype hlID inputsave searchpair or screenrow line settabvar histadd deepcopy strpart remote_peek and eval getftime submatch screenchar winsaveview matchadd mkdir screenattr getfontname libcall reltimestr getfsize winnr invert pow getbufline byte2line soundfold repeat fnameescape tagfiles sin strwidth spellbadword trunc maparg log lispindent hostname setpos globpath remote_foreground getchar synIDattr fnamemodify cscope_connection stridx winbufnr indent min complete_add nr2char searchpairpos inputdialog values matchlist items hlexists strridx browsedir expand fmod pathshorten line2byte argc count getwinvar glob foldtextresult getreg foreground cosh matchdelete has char2nr simplify histget searchdecl iconv winrestcmd pumvisible writefile foldlevel haslocaldir keys cos matchstr foldtext histnr tan tempname getcwd byteidx getbufvar islocked escape eventhandler remote_send serverlist winrestview synstack pyeval prevnonblank readfile cindent filereadable changenr exp"},
-illegal:/;/,contains:[a.NUMBER_MODE,a.APOS_STRING_MODE,{className:"string",begin:/"(\\"|\n\\|[^"\n])*"/},a.COMMENT('"',"$"),{className:"variable",begin:/[bwtglsav]:[\w\d_]*/},{className:"function",beginKeywords:"function function!",end:"$",relevance:0,contains:[a.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{className:"symbol",begin:/<[\w-]+>/}]}});b.registerLanguage("x86asm",function(a){return{case_insensitive:!0,lexemes:"[.%]?"+a.IDENT_RE,keywords:{keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",
+illegal:/;/,contains:[a.NUMBER_MODE,{className:"string",begin:"'",end:"'",illegal:"\\n"},{className:"string",begin:/"(\\"|\n\\|[^"\n])*"/},a.COMMENT('"',"$"),{className:"variable",begin:/[bwtglsav]:[\w\d_]*/},{className:"function",beginKeywords:"function function!",end:"$",relevance:0,contains:[a.TITLE_MODE,{className:"params",begin:"\\(",end:"\\)"}]},{className:"symbol",begin:/<[\w-]+>/}]}});b.registerLanguage("x86asm",function(a){return{case_insensitive:!0,lexemes:"[.%]?"+a.IDENT_RE,keywords:{keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",
 built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0  xmm1  xmm2  xmm3  xmm4  xmm5  xmm6  xmm7  xmm8  xmm9 xmm10  xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0  ymm1  ymm2  ymm3  ymm4  ymm5  ymm6  ymm7  ymm8  ymm9 ymm10  ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0  zmm1  zmm2  zmm3  zmm4  zmm5  zmm6  zmm7  zmm8  zmm9 zmm10  zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",
 meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__  __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__  __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},
 contains:[a.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",
 end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}});b.registerLanguage("xl",function(a){var b={keyword:"if then else do while until for loop import with is as where when by data constant integer real text name boolean symbol infix prefix postfix block tree",
 literal:"true false nil",built_in:"in mod rem and or xor not abs sign floor ceil sqrt sin cos tan asin acos atan exp expm1 log log2 log10 log1p pi at text_length text_range text_find text_replace contains page slide basic_slide title_slide title subtitle fade_in fade_out fade_at clear_color color line_color line_width texture_wrap texture_transform texture scale_?x scale_?y scale_?z? translate_?x translate_?y translate_?z? rotate_?x rotate_?y rotate_?z? rectangle circle ellipse sphere path line_to move_to quad_to curve_to theme background contents locally time mouse_?x mouse_?y mouse_buttons ObjectLoader Animate MovieCredits Slides Filters Shading Materials LensFlare Mapping VLCAudioVideo StereoDecoder PointCloud NetworkAccess RemoteControl RegExp ChromaKey Snowfall NodeJS Speech Charts"},
 d={className:"string",begin:'"',end:'"',illegal:"\\n"},e={beginKeywords:"import",end:"$",keywords:b,contains:[d]},f={className:"function",begin:/[a-z][^\n]*->/,returnBegin:!0,end:/->/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,keywords:b}})]};return{aliases:["tao"],lexemes:/[a-zA-Z][a-zA-Z0-9_?]*/,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,{className:"string",begin:"'",end:"'",illegal:"\\n"},{className:"string",begin:"<<",end:">>"},f,e,{className:"number",
-begin:"[0-9]+#[0-9A-Z_]+(\\.[0-9-A-Z_]+)?#?([Ee][+-]?[0-9]+)?"},a.NUMBER_MODE]}});b.registerLanguage("xquery",function(a){a={begin:"{",end:"}"};var b=[{begin:/\$[a-zA-Z0-9\-]+/},{className:"string",variants:[{begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{className:"comment",begin:"\\(:",end:":\\)",relevance:10,contains:[{className:"doctag",
-begin:"@\\w+"}]},{className:"meta",begin:"%\\w+"},a];a.contains=b;return{aliases:["xpath","xq"],case_insensitive:!1,lexemes:/[a-zA-Z\$][a-zA-Z0-9_:\-]*/,illegal:/(proc)|(abstract)|(extends)|(until)|(#)/,keywords:{keyword:"for let if while then else return where group by xquery encoding versionmodule namespace boundary-space preserve strip default collation base-uri orderingcopy-namespaces order declare import schema namespace function option in allowing emptyat tumbling window sliding window start when only end when previous next stable ascendingdescending empty greatest least some every satisfies switch case typeswitch try catch andor to union intersect instance of treat as castable cast map array delete insert intoreplace value rename copy modify update",
-literal:"false true xs:string xs:integer element item xs:date xs:datetime xs:float xs:double xs:decimal QName xs:anyURI xs:long xs:int xs:short xs:byte attribute"},contains:b}});b.registerLanguage("zephir",function(a){var b={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["zep"],case_insensitive:!0,
-keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var let while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally int uint long ulong char uchar double float bool boolean stringlikely unlikely",
+begin:"[0-9]+#[0-9A-Z_]+(\\.[0-9-A-Z_]+)?#?([Ee][+-]?[0-9]+)?"},a.NUMBER_MODE]}});b.registerLanguage("xquery",function(a){return{aliases:["xpath","xq"],case_insensitive:!1,lexemes:/[a-zA-Z\$][a-zA-Z0-9_:\-]*/,illegal:/(proc)|(abstract)|(extends)|(until)|(#)/,keywords:{keyword:"module schema namespace boundary-space preserve no-preserve strip default collation base-uri ordering context decimal-format decimal-separator copy-namespaces empty-sequence except exponent-separator external grouping-separator inherit no-inherit lax minus-sign per-mille percent schema-attribute schema-element strict unordered zero-digit declare import option function validate variable for at in let where order group by return if then else tumbling sliding window start when only end previous next stable ascending descending allowing empty greatest least some every satisfies switch case typeswitch try catch and or to union intersect instance of treat as castable cast map array delete insert into replace value rename copy modify update",
+type:"item document-node node attribute document element comment namespace namespace-node processing-instruction text construction xs:anyAtomicType xs:untypedAtomic xs:duration xs:time xs:decimal xs:float xs:double xs:gYearMonth xs:gYear xs:gMonthDay xs:gMonth xs:gDay xs:boolean xs:base64Binary xs:hexBinary xs:anyURI xs:QName xs:NOTATION xs:dateTime xs:dateTimeStamp xs:date xs:string xs:normalizedString xs:token xs:language xs:NMTOKEN xs:Name xs:NCName xs:ID xs:IDREF xs:ENTITY xs:integer xs:nonPositiveInteger xs:negativeInteger xs:long xs:int xs:short xs:byte xs:nonNegativeInteger xs:unisignedLong xs:unsignedInt xs:unsignedShort xs:unsignedByte xs:positiveInteger xs:yearMonthDuration xs:dayTimeDuration",
+literal:"eq ne lt le gt ge is self:: child:: descendant:: descendant-or-self:: attribute:: following:: following-sibling:: parent:: ancestor:: ancestor-or-self:: preceding:: preceding-sibling:: NaN"},contains:[{className:"variable",begin:/[\$][\w-:]+/},{className:"built_in",variants:[{begin:/\barray:/,end:/(?:append|filter|flatten|fold\-(?:left|right)|for-each(?:\-pair)?|get|head|insert\-before|join|put|remove|reverse|size|sort|subarray|tail)\b/},{begin:/\bmap:/,end:/(?:contains|entry|find|for\-each|get|keys|merge|put|remove|size)\b/},
+{begin:/\bmath:/,end:/(?:a(?:cos|sin|tan[2]?)|cos|exp(?:10)?|log(?:10)?|pi|pow|sin|sqrt|tan)\b/},{begin:/\bop:/,end:/\(/,excludeEnd:!0},{begin:/\bfn:/,end:/\(/,excludeEnd:!0},{begin:/[^<\/\$:'"-]\b(?:abs|accumulator\-(?:after|before)|adjust\-(?:date(?:Time)?|time)\-to\-timezone|analyze\-string|apply|available\-(?:environment\-variables|system\-properties)|avg|base\-uri|boolean|ceiling|codepoints?\-(?:equal|to\-string)|collation\-key|collection|compare|concat|contains(?:\-token)?|copy\-of|count|current(?:\-)?(?:date(?:Time)?|time|group(?:ing\-key)?|output\-uri|merge\-(?:group|key))?data|dateTime|days?\-from\-(?:date(?:Time)?|duration)|deep\-equal|default\-(?:collation|language)|distinct\-values|document(?:\-uri)?|doc(?:\-available)?|element\-(?:available|with\-id)|empty|encode\-for\-uri|ends\-with|environment\-variable|error|escape\-html\-uri|exactly\-one|exists|false|filter|floor|fold\-(?:left|right)|for\-each(?:\-pair)?|format\-(?:date(?:Time)?|time|integer|number)|function\-(?:arity|available|lookup|name)|generate\-id|has\-children|head|hours\-from\-(?:dateTime|duration|time)|id(?:ref)?|implicit\-timezone|in\-scope\-prefixes|index\-of|innermost|insert\-before|iri\-to\-uri|json\-(?:doc|to\-xml)|key|lang|last|load\-xquery\-module|local\-name(?:\-from\-QName)?|(?:lower|upper)\-case|matches|max|minutes\-from\-(?:dateTime|duration|time)|min|months?\-from\-(?:date(?:Time)?|duration)|name(?:space\-uri\-?(?:for\-prefix|from\-QName)?)?|nilled|node\-name|normalize\-(?:space|unicode)|not|number|one\-or\-more|outermost|parse\-(?:ietf\-date|json)|path|position|(?:prefix\-from\-)?QName|random\-number\-generator|regex\-group|remove|replace|resolve\-(?:QName|uri)|reverse|root|round(?:\-half\-to\-even)?|seconds\-from\-(?:dateTime|duration|time)|snapshot|sort|starts\-with|static\-base\-uri|stream\-available|string\-?(?:join|length|to\-codepoints)?|subsequence|substring\-?(?:after|before)?|sum|system\-property|tail|timezone\-from\-(?:date(?:Time)?|time)|tokenize|trace|trans(?:form|late)|true|type\-available|unordered|unparsed\-(?:entity|text)?\-?(?:public\-id|uri|available|lines)?|uri\-collection|xml\-to\-json|years?\-from\-(?:date(?:Time)?|duration)|zero\-or\-one)\b/},
+{begin:/\blocal:/,end:/\(/,excludeEnd:!0},{begin:/\bzip:/,end:/(?:zip\-file|(?:xml|html|text|binary)\-entry| (?:update\-)?entries)\b/},{begin:/\b(?:util|db|functx|app|xdmp|xmldb):/,end:/\(/,excludeEnd:!0}]},{className:"string",variants:[{begin:/"/,end:/"/,contains:[{begin:/""/,relevance:0}]},{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{className:"comment",begin:"\\(:",end:":\\)",
+relevance:10,contains:[{className:"doctag",begin:"@\\w+"}]},{className:"meta",begin:/%[\w-:]+/},{className:"title",begin:/\bxquery version "[13]\.[01]"\s?(?:encoding ".+")?/,end:/;/},{beginKeywords:"element attribute comment document processing-instruction",end:"{",excludeEnd:!0},{begin:/<([\w\._:\-]+)((\s*.*)=('|").*('|"))?>/,end:/(\/[\w\._:\-]+>)/,subLanguage:"xml",contains:[{begin:"{",end:"}",subLanguage:"xquery"},"self"]}]}});b.registerLanguage("zephir",function(a){var b={className:"string",contains:[a.BACKSLASH_ESCAPE],
+variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["zep"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var let while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally int uint long ulong char uchar double float bool boolean stringlikely unlikely",
 contains:[a.C_LINE_COMMENT_MODE,a.HASH_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:"<<<['\"]?\\w+['\"]?$",end:"^\\w+;",contains:[a.BACKSLASH_ESCAPE]},{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,
 {className:"params",begin:"\\(",end:"\\)",contains:["self",a.C_BLOCK_COMMENT_MODE,b,d]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"=>"},b,d]}});return b});
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 33489a2..b3b73ad 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,4 +1,4 @@
-load("//tools/bzl:maven_jar.bzl", "ECLIPSE", "GERRIT", "MAVEN_CENTRAL", "MAVEN_LOCAL", "maven_jar")
+load("//tools/bzl:maven_jar.bzl", "MAVEN_CENTRAL", "maven_jar")
 
 _JGIT_VERS = "5.2.1.201812262042-r"
 
@@ -6,7 +6,7 @@
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
-_JGIT_REPO = ECLIPSE  # Leave here even if set to MAVEN_CENTRAL.
+_JGIT_REPO = MAVEN_CENTRAL  # Leave here even if set to MAVEN_CENTRAL.
 
 # set this to use a local version.
 # "/home/<user>/projects/jgit"
@@ -67,11 +67,11 @@
 
 def jgit_dep(name):
     mapping = {
-        "@jgit-junit//jar": "@jgit//org.eclipse.jgit.junit:junit",
-        "@jgit-lib//jar:src": "@jgit//org.eclipse.jgit:libjgit-src.jar",
-        "@jgit-lib//jar": "@jgit//org.eclipse.jgit:jgit",
-        "@jgit-servlet//jar": "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
         "@jgit-archive//jar": "@jgit//org.eclipse.jgit.archive:jgit-archive",
+        "@jgit-junit//jar": "@jgit//org.eclipse.jgit.junit:junit",
+        "@jgit-lib//jar": "@jgit//org.eclipse.jgit:jgit",
+        "@jgit-lib//jar:src": "@jgit//org.eclipse.jgit:libjgit-src.jar",
+        "@jgit-servlet//jar": "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
     }
 
     if LOCAL_JGIT_REPO:
diff --git a/lib/js/BUILD b/lib/js/BUILD
index 706c472..7478ef3 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -1,7 +1,8 @@
-package(default_visibility = ["//visibility:public"])
-
+load("//lib/js:bower_components.bzl", "define_bower_components")
 load("//tools/bzl:js.bzl", "bower_component", "js_component")
 
+package(default_visibility = ["//visibility:public"])
+
 # For importing new versions of existing bower packages,
 #
 # 1) edit the versions of 'seed' components in WORKSPACE as desired
@@ -20,8 +21,6 @@
 # 4) remove bower_component(name="my_new_dependency", .. ) here
 #
 
-load("//lib/js:bower_components.bzl", "define_bower_components")
-
 define_bower_components()
 
 js_component(
@@ -46,3 +45,8 @@
     name = "codemirror-minified",
     license = "//lib:LICENSE-codemirror-minified",
 )
+
+bower_component(
+    name = "resemblejs",
+    license = "//lib:LICENSE-resemblejs",
+)
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index 75c8277..48529a0 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -124,8 +124,8 @@
     bower_archive(
         name = "paper-icon-button",
         package = "PolymerElements/paper-icon-button",
-        version = "2.2.0",
-        sha1 = "9525e76ef433428bb9d6ec4fa65c4ef83156a803",
+        version = "2.2.1",
+        sha1 = "68f76af3a9379f256a3900a4b68d871898f1fe57",
     )
     bower_archive(
         name = "paper-ripple",
diff --git a/lib/js/npm.bzl b/lib/js/npm.bzl
index 92f44bd..8a9e1ee 100644
--- a/lib/js/npm.bzl
+++ b/lib/js/npm.bzl
@@ -1,11 +1,11 @@
 NPM_VERSIONS = {
-    "bower": "1.8.2",
+    "bower": "1.8.8",
     "crisper": "2.0.2",
     "polymer-bundler": "4.0.2",
 }
 
 NPM_SHA1S = {
-    "bower": "adf53529c8d4af02ef24fb8d5341c1419d33e2f7",
+    "bower": "82544be34a33aeae7efb8bdf9905247b2cffa985",
     "crisper": "7183c58cea33632fb036c91cefd1b43e390d22a2",
     "polymer-bundler": "6b296b6099ab5a0e93ca914cbe93e753f2395910",
 }
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index eab2ac8..adb5030 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -1,7 +1,7 @@
-package(default_visibility = ["//visibility:public"])
-
 load("//tools/bzl:maven.bzl", "merge_maven_jars")
 
+package(default_visibility = ["//visibility:public"])
+
 # core and backward-codecs both provide
 # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
 merge_maven_jars(
diff --git a/lib/polymer_externs/BUILD b/lib/polymer_externs/BUILD
index cd71d64..f07aa2f 100644
--- a/lib/polymer_externs/BUILD
+++ b/lib/polymer_externs/BUILD
@@ -12,10 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-package(default_visibility = ["//visibility:public"])
-
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
 
+package(default_visibility = ["//visibility:public"])
+
 closure_js_library(
     name = "polymer_closure",
     srcs = ["@polymer_closure//file"],
diff --git a/lib/zlib/BUILD b/lib/zlib/BUILD
new file mode 100644
index 0000000..f948117
--- /dev/null
+++ b/lib/zlib/BUILD
@@ -0,0 +1,60 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # BSD/MIT-like license (for zlib)
+
+_ZLIB_HEADERS = [
+    "crc32.h",
+    "deflate.h",
+    "gzguts.h",
+    "inffast.h",
+    "inffixed.h",
+    "inflate.h",
+    "inftrees.h",
+    "trees.h",
+    "zconf.h",
+    "zlib.h",
+    "zutil.h",
+]
+
+_ZLIB_PREFIXED_HEADERS = ["zlib/include/" + hdr for hdr in _ZLIB_HEADERS]
+
+# In order to limit the damage from the `includes` propagation
+# via `:zlib`, copy the public headers to a subdirectory and
+# expose those.
+genrule(
+    name = "copy_public_headers",
+    srcs = _ZLIB_HEADERS,
+    outs = _ZLIB_PREFIXED_HEADERS,
+    cmd = "cp $(SRCS) $(@D)/zlib/include/",
+    visibility = ["//visibility:private"],
+)
+
+cc_library(
+    name = "zlib",
+    srcs = [
+        "adler32.c",
+        "compress.c",
+        "crc32.c",
+        "deflate.c",
+        "gzclose.c",
+        "gzlib.c",
+        "gzread.c",
+        "gzwrite.c",
+        "infback.c",
+        "inffast.c",
+        "inflate.c",
+        "inftrees.c",
+        "trees.c",
+        "uncompr.c",
+        "zutil.c",
+        # Include the un-prefixed headers in srcs to work
+        # around the fact that zlib isn't consistent in its
+        # choice of <> or "" delimiter when including itself.
+    ] + _ZLIB_HEADERS,
+    hdrs = _ZLIB_PREFIXED_HEADERS,
+    copts = [
+        "-Wno-unused-variable",
+        "-Wno-implicit-function-declaration",
+    ],
+    includes = ["zlib/include/"],
+)
diff --git a/plugins/BUILD b/plugins/BUILD
index bddff41..8baa3c7 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -1,4 +1,5 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:javadoc.bzl", "java_doc")
 load(
     "//tools/bzl:plugins.bzl",
     "CORE_PLUGINS",
@@ -30,6 +31,7 @@
     "//antlr3:query_parser",
     "//java/com/google/gerrit/common:annotations",
     "//java/com/google/gerrit/common:server",
+    "//java/com/google/gerrit/exceptions",
     "//java/com/google/gerrit/extensions:api",
     "//java/com/google/gerrit/git",
     "//java/com/google/gerrit/index",
@@ -37,15 +39,20 @@
     "//java/com/google/gerrit/index:query_exception",
     "//java/com/google/gerrit/json",
     "//java/com/google/gerrit/lifecycle",
+    "//java/com/google/gerrit/mail",
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/metrics/dropwizard",
     "//java/com/google/gerrit/reviewdb:server",
+    "//java/com/google/gerrit/server/api",
     "//java/com/google/gerrit/server/audit",
+    "//java/com/google/gerrit/server/cache/mem",
     "//java/com/google/gerrit/server/logging",
     "//java/com/google/gerrit/server/schema",
     "//java/com/google/gerrit/server/util/time",
     "//java/com/google/gerrit/util/cli",
     "//java/com/google/gerrit/util/http",
+    "//lib/antlr:java-runtime",
+    "//lib/auto:auto-value-annotations",
     "//lib/commons:compress",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
@@ -72,7 +79,6 @@
     "//lib:guava",
     "//lib:guava-retrying",
     "//lib:gson",
-    "//lib:gwtorm",
     "//lib:icu4j",
     "//lib:jsch",
     "//lib:mime-util",
@@ -123,8 +129,6 @@
     ],
 )
 
-load("//tools/bzl:javadoc.bzl", "java_doc")
-
 java_doc(
     name = "plugin-api-javadoc",
     libs = PLUGIN_API + [
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 22342a6..3baf62e 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 22342a6da26c75b14bc629331c339d1b820b4d39
+Subproject commit 3baf62e1f12bea107598777a3881455cf39fd8ab
diff --git a/plugins/delete-project b/plugins/delete-project
index 09269fd..4401de0 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 09269fd13109ef6771803fd0560a78e34dd5281e
+Subproject commit 4401de09767d6e8655d29377f95145840a73feaf
diff --git a/plugins/download-commands b/plugins/download-commands
index edd7156..697ca37 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit edd715618415d9a5e03b4555d9a2d3cca8fff6e8
+Subproject commit 697ca3783add1c8fa406d60f7821ab6db7c1bfec
diff --git a/plugins/gitiles b/plugins/gitiles
new file mode 160000
index 0000000..cd9cddd
--- /dev/null
+++ b/plugins/gitiles
@@ -0,0 +1 @@
+Subproject commit cd9cdddfb0747065b532817f8eca27bb5ed04478
diff --git a/plugins/hooks b/plugins/hooks
index 9d0ad9a..4928874 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 9d0ad9ae5667b7da5bb3e7e8066d2dbff446d70b
+Subproject commit 492887449bf66b0f828aa26506629a8f5838f4d3
diff --git a/plugins/replication b/plugins/replication
index 3a470d7..6a4105d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 3a470d7b5268dfd3f8c2f95a7e0c493f8fb3629a
+Subproject commit 6a4105d7f3dc09fe2eb7cb15901178eab7723a44
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index a9456bf..3dec016 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit a9456bfdb862dfa7197583decac3c22149ae8109
+Subproject commit 3dec016f9186131b2359d689a3e4a2c14717710e
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 17f5d01..25bcffa 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 17f5d016b8c9e76cdbf467a004ba22d4c46311fa
+Subproject commit 25bcffa8e0a35df7f62b70b7dd47d2284f3a777a
diff --git a/plugins/webhooks b/plugins/webhooks
index 48703a1..93bcec5 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 48703a1cf5dce6af87f700ccda2c6f5949a6fa1c
+Subproject commit 93bcec540df35f0277240bc63bf5cc5f306e4ddd
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 5889ffd..0e9b4bb 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,8 +1,8 @@
-package(default_visibility = ["//visibility:public"])
-
 load("@io_bazel_rules_go//go:def.bzl", "go_binary")
-load("//tools/bzl:js.bzl", "bower_component_bundle")
 load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "bower_component_bundle")
+
+package(default_visibility = ["//visibility:public"])
 
 bower_component_bundle(
     name = "polygerrit_components.bower_components",
@@ -57,11 +57,8 @@
     data = [
         ":fonts.zip",
         "//polygerrit-ui/app:test_components.zip",
-        "//resources/com/google/gerrit/httpd/raw",
     ],
     deps = [
-        "@com_github_robfig_soy//:go_default_library",
-        "@com_github_robfig_soy//soyhtml:go_default_library",
         "@org_golang_x_tools//godoc/vfs/httpfs:go_default_library",
         "@org_golang_x_tools//godoc/vfs/zipfs:go_default_library",
     ],
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 943c328..6244512 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -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/BUILD b/polygerrit-ui/app/BUILD
index d48641b..19bdcee 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -1,8 +1,8 @@
-package(default_visibility = ["//visibility:public"])
-
-load(":rules.bzl", "polygerrit_bundle")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("//tools/bzl:js.bzl", "bower_component_bundle")
+load(":rules.bzl", "polygerrit_bundle")
+
+package(default_visibility = ["//visibility:public"])
 
 polygerrit_bundle(
     name = "polygerrit_ui",
@@ -51,6 +51,7 @@
         [
             "bower_components/**/*.html",
             "bower_components/**/*.js",
+            "bower_components/**/*.map",
         ],
     ),
 )
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 4b670da..c21e96f 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -52,6 +52,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.BaseUrlBehavior,
         ],
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index e92eb49..96d4a08 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -38,6 +38,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'docs-url-behavior-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.DocsUrlBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
index 640d902..e445a78 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -46,6 +46,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.DomUtilBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
index 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-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
index 929834f..0b37a0d 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -38,6 +38,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.AccessBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index a1902cd..7c23c00 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -41,6 +41,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.AdminNavBehavior,
         ],
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
index 3ac94fe..820d6bc 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
@@ -44,6 +44,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element-anon',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.AnonymousNameBehavior,
         ],
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index e8cbb09..b052d06 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -48,6 +48,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.ChangeTableBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
index 54b979f..f6c765f 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -40,6 +40,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.ListViewBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 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/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index 8bca339..943e000 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -51,6 +51,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'tooltip-behavior-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.TooltipBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
index 73dda1b..d909e86 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -40,6 +40,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.URLEncodingBehavior],
       });
     });
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index dac90f8..d013299 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -50,6 +50,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.KeyboardShortcutBehavior],
         keyBindings: {
           k: '_handleKey',
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 49d90f0..6af43dc 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -54,6 +54,7 @@
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
+        _legacyUndefinedCheck: true,
         behaviors: [
           Gerrit.BaseUrlBehavior,
           Gerrit.RESTClientBehavior,
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
index bc16b39..6e040a3 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -39,6 +39,7 @@
     suiteSetup(() => {
       Polymer({
         is: 'safe-types-element',
+        _legacyUndefinedCheck: true,
         behaviors: [Gerrit.SafeTypes],
       });
     });
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index 6fb7b0e..6adf127 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -38,6 +38,7 @@
 
   Polymer({
     is: 'gr-access-section',
+    _legacyUndefinedCheck: true,
 
     properties: {
       capabilities: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 4073798..aca691a 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-admin-group-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -129,8 +130,7 @@
     },
 
     _refreshGroupsList() {
-      this.$.restAPI.invalidateGroupsCache(this._filter,
-          this._groupsPerPage, this._offset);
+      this.$.restAPI.invalidateGroupsCache();
       return this._getGroups(this._filter, this._groupsPerPage,
           this._offset);
     },
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 3d430c2..4d7fd0c 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-admin-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index b5dcf63..9c0e405 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -25,6 +25,7 @@
 
   Polymer({
     is: 'gr-confirm-delete-item-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index 987b63d..da1c871 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -32,9 +32,6 @@
   <template>
     <style include="shared-styles"></style>
     <style include="gr-form-styles">
-      :host {
-        display: inline-block;
-      }
       input:not([type="checkbox"]),
       gr-autocomplete,
       iron-autogrow-textarea {
@@ -43,13 +40,6 @@
       .value {
         width: 32em;
       }
-      section {
-        align-items: center;
-        display: flex;
-      }
-      #description {
-        align-items: initial;
-      }
       gr-autocomplete {
         --gr-autocomplete: {
           padding: 0 .15em;
@@ -58,68 +48,71 @@
       .hide {
         display: none;
       }
+      @media only screen and (max-width: 40em) {
+        .value {
+          width: 29em;
+        }
+      }
     </style>
     <div class="gr-form-styles">
-      <div id="form">
-        <section class$="[[_computeBranchClass(baseChange)]]">
-          <span class="title">Select branch for new change</span>
-          <span class="value">
-            <gr-autocomplete
-                id="branchInput"
-                text="{{branch}}"
-                query="[[_query]]"
-                placeholder="Destination branch">
-            </gr-autocomplete>
-          </span>
-        </section>
-        <section class$="[[_computeBranchClass(baseChange)]]">
-          <span class="title">Provide base commit sha1 for change</span>
-          <span class="value">
-            <input
-                is="iron-input"
-                id="baseCommitInput"
-                maxlength="40"
-                placeholder="(optional)"
-                bind-value="{{baseCommit}}">
-          </span>
-        </section>
-        <section>
-          <span class="title">Enter topic for new change</span>
-          <span class="value">
-            <input
-                is="iron-input"
-                id="tagNameInput"
-                maxlength="1024"
-                placeholder="(optional)"
-                bind-value="{{topic}}">
-          </span>
-        </section>
-        <section id="description">
-          <span class="title">Description</span>
-          <span class="value">
-            <iron-autogrow-textarea
-                id="messageInput"
-                class="message"
-                autocomplete="on"
-                rows="4"
-                max-rows="15"
-                bind-value="{{subject}}"
-                placeholder="Insert the description of the change.">
-            </iron-autogrow-textarea>
-          </span>
-        </section>
-        <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
-          <label
-              class="title"
-              for="privateChangeCheckBox">Private change</label>
-          <span class="value">
-            <input
-                type="checkbox"
-                id="privateChangeCheckBox"
-                checked$="[[_formatBooleanString(privateByDefault)]]">
-          </span>
-        </section>
-      </div>
+      <section class$="[[_computeBranchClass(baseChange)]]">
+        <span class="title">Select branch for new change</span>
+        <span class="value">
+          <gr-autocomplete
+              id="branchInput"
+              text="{{branch}}"
+              query="[[_query]]"
+              placeholder="Destination branch">
+          </gr-autocomplete>
+        </span>
+      </section>
+      <section class$="[[_computeBranchClass(baseChange)]]">
+        <span class="title">Provide base commit sha1 for change</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              id="baseCommitInput"
+              maxlength="40"
+              placeholder="(optional)"
+              bind-value="{{baseCommit}}">
+        </span>
+      </section>
+      <section>
+        <span class="title">Enter topic for new change</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              id="tagNameInput"
+              maxlength="1024"
+              placeholder="(optional)"
+              bind-value="{{topic}}">
+        </span>
+      </section>
+      <section id="description">
+        <span class="title">Description</span>
+        <span class="value">
+          <iron-autogrow-textarea
+              id="messageInput"
+              class="message"
+              autocomplete="on"
+              rows="4"
+              max-rows="15"
+              bind-value="{{subject}}"
+              placeholder="Insert the description of the change.">
+          </iron-autogrow-textarea>
+        </span>
+      </section>
+      <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
+        <label
+            class="title"
+            for="privateChangeCheckBox">Private change</label>
+        <span class="value">
+          <input
+              type="checkbox"
+              id="privateChangeCheckBox"
+              checked$="[[_formatBooleanString(privateByDefault)]]">
+        </span>
+      </section>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 8e15755..7eec959 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-create-change-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repoName: String,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 01aeb43..ec667ee 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-group-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 4e9da90..65bb46d 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -24,6 +24,7 @@
 
   Polymer({
     is: 'gr-create-pointer-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       detailType: String,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index bb2b5f2..ef7edd4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-repo-dialog',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index bc6a5d0..966f3c9 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-group-audit-log',
+    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: String,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index c698617..76bd558 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -25,6 +25,7 @@
 
   Polymer({
     is: 'gr-group-members',
+    _legacyUndefinedCheck: true,
 
     properties: {
       groupId: Number,
@@ -199,7 +200,7 @@
 
     _handleSavingIncludedGroups() {
       return this.$.restAPI.saveIncludedGroup(this._groupName,
-          this._includedGroupSearchId, err => {
+          this._includedGroupSearchId.replace(/\+/g, ' '), err => {
             if (err.status === 404) {
               this.dispatchEvent(new CustomEvent('show-alert', {
                 detail: {message: SAVING_ERROR_TEXT},
@@ -223,7 +224,7 @@
     },
 
     _handleDeleteIncludedGroup(e) {
-      const id = decodeURIComponent(e.model.get('item.id'));
+      const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
       const name = e.model.get('item.name');
       const item = name || id;
       if (!item) { return ''; }
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/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index c66d860..095a0f9 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -32,6 +32,7 @@
 
   Polymer({
     is: 'gr-group',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the group name changes.
@@ -180,13 +181,10 @@
     },
 
     _handleSaveOptions() {
-      let options;
-      // The value is in string so we have to convert it to a boolean.
-      if (this._groupConfig.options.visible_to_all) {
-        options = {visible_to_all: true};
-      } else if (!this._groupConfig.options.visible_to_all) {
-        options = {visible_to_all: false};
-      }
+      const visible = this._groupConfig.options.visible_to_all;
+
+      const options = {visible_to_all: visible};
+
       return this.$.restAPI.saveGroupOptions(this.groupId,
           options).then(config => {
             this._options = false;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index 22f461b..a5bb5fd 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -109,6 +109,7 @@
               items="{{_rules}}"
               as="rule">
             <gr-rule-editor
+                has-range="[[_computeHasRange(name)]]"
                 label="[[_label]]"
                 editing="[[editing]]"
                 group-id="[[rule.id]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 31d371d..a131f4f 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -19,6 +19,11 @@
 
   const MAX_AUTOCOMPLETE_RESULTS = 20;
 
+  const RANGE_NAMES = [
+    'QUERY LIMIT',
+    'BATCH CHANGES LIMIT',
+  ];
+
   /**
    * Fired when the permission has been modified or removed.
    *
@@ -32,6 +37,7 @@
 
   Polymer({
     is: 'gr-permission',
+    _legacyUndefinedCheck: true,
 
     properties: {
       labels: Object,
@@ -244,7 +250,7 @@
     _handleAddRuleItem(e) {
       // The group id is encoded, but have to decode in order for the access
       // API to work as expected.
-      const groupId = decodeURIComponent(e.detail.value.id);
+      const groupId = decodeURIComponent(e.detail.value.id).replace(/\+/g, ' ');
       this.set(['permission', 'value', 'rules', groupId], {});
 
       // Purposely don't recompute sorted array so that the newly added rule
@@ -269,5 +275,11 @@
       this.set(['permission', 'value', 'rules', groupId], value);
       this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
     },
+
+    _computeHasRange(name) {
+      if (!name) { return false; }
+
+      return RANGE_NAMES.includes(name.toUpperCase());
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index e29c4a2..5f1f9b5 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -255,6 +255,14 @@
         assert.isFalse(element._deleted);
         assert.isNotOk(element.permission.value.deleted);
       });
+
+      test('_computeHasRange', () => {
+        assert.isTrue(element._computeHasRange('Query Limit'));
+
+        assert.isTrue(element._computeHasRange('Batch Changes Limit'));
+
+        assert.isFalse(element._computeHasRange('test'));
+      });
     });
 
     suite('interactions', () => {
@@ -302,11 +310,11 @@
         element.name = 'Priority';
         element.section = 'refs/*';
         element.groups = {};
-        element.$.groupAutocomplete.text = 'new group name';
+        element.$.groupAutocomplete.text = 'ldap/tests tests';
         const e = {
           detail: {
             value: {
-              id: 'newUserGroupId',
+              id: 'ldap:CN=test+test',
             },
           },
         };
@@ -315,11 +323,11 @@
         assert.equal(Object.keys(element._groupsWithRules).length, 2);
         element._handleAddRuleItem(e);
         flushAsynchronousOperations();
-        assert.deepEqual(element.groups, {newUserGroupId: {
-          name: 'new group name'}});
+        assert.deepEqual(element.groups, {'ldap:CN=test test': {
+          name: 'ldap/tests tests'}});
         assert.equal(element._rules.length, 3);
         assert.equal(Object.keys(element._groupsWithRules).length, 3);
-        assert.deepEqual(element.permission.value.rules['newUserGroupId'],
+        assert.deepEqual(element.permission.value.rules['ldap:CN=test test'],
             {action: 'ALLOW', min: -2, max: 2, added: true});
         // New rule should be removed if cancel from editing.
         element.editing = false;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 6cbc94a..d533f98 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-plugin-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index fe043d5..713bf02 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -21,7 +21,7 @@
 
   const NOTHING_TO_SAVE = 'No changes to save.';
 
-  const MAX_AUTOCOMPLETE_RESULTS = 20;
+  const MAX_AUTOCOMPLETE_RESULTS = 50;
 
   /**
    * Fired when save is a no-op
@@ -70,6 +70,7 @@
 
   Polymer({
     is: 'gr-repo-access',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index bcdb7f6..e0becaf 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-command',
+    _legacyUndefinedCheck: true,
 
     properties: {
       title: String,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index a25055e..92b11fc 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -28,6 +28,7 @@
 
   Polymer({
     is: 'gr-repo-commands',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 7dea7f4..72ee83d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-dashboards',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index 8512a5d..8b91977 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -26,6 +26,7 @@
 
   Polymer({
     is: 'gr-repo-detail-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -62,7 +63,7 @@
       /**
        * Because  we request one more than the projectsPerPage, _shownProjects
        * maybe one less than _projects.
-       * */
+       */
       _shownItems: {
         type: Array,
         computed: 'computeShownItems(_items)',
@@ -90,7 +91,7 @@
     _determineIfOwner(repo) {
       return this.$.restAPI.getRepoAccess(repo)
           .then(access =>
-                this._isOwner = access && access[repo].is_owner);
+                this._isOwner = access && !!access[repo].is_owner);
     },
 
     _paramsChanged(params) {
@@ -194,6 +195,11 @@
         if (res.status < 400) {
           this._isEditing = false;
           e.model.set('item.revision', ref);
+          // This is needed to refresh _items property with fresh data,
+          // specifically can_delete from the json response.
+          this._getItems(
+              this._filter, this._repo, this._itemsPerPage,
+              this._offset, this.detailType);
         }
       });
     },
@@ -240,10 +246,11 @@
       this.$.overlay.open();
     },
 
-    _computeHideDeleteClass(owner, deleteRef) {
-      if (owner && !deleteRef || owner && deleteRef || deleteRef || owner) {
+    _computeHideDeleteClass(owner, canDelete) {
+      if (canDelete || owner) {
         return 'show';
       }
+
       return '';
     },
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index 24edadc..5d2a9ad 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -549,5 +549,11 @@
         element._paramsChanged(params);
       });
     });
+
+    test('test _computeHideDeleteClass', () => {
+      assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
+      assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
+      assert.deepEqual(element._computeHideDeleteClass(false, false), '');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index 116f084..6b46cef 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
@@ -130,8 +131,7 @@
     },
 
     _refreshReposList() {
-      this.$.restAPI.invalidateReposCache(this._filter,
-          this._reposPerPage, this._offset);
+      this.$.restAPI.invalidateReposCache();
       return this._getRepos(this._filter, this._reposPerPage,
           this._offset);
     },
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 77a1e2a..840a529 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -53,6 +53,7 @@
 
   Polymer({
     is: 'gr-repo',
+    _legacyUndefinedCheck: true,
 
     properties: {
       params: Object,
@@ -307,8 +308,8 @@
         commands.push({
           title,
           command: commandObj[title]
-              .replace('${project}', encodeURI(repo))
-              .replace('${project-base-name}',
+              .replace(/\$\{project\}/gi, encodeURI(repo))
+              .replace(/\$\{project-base-name\}/gi,
               encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
         });
       }
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index d59deed4..c8ae650 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -71,7 +71,11 @@
         color: var(--deemphasized-text-color);
       }
     </style>
-    <style include="gr-form-styles"></style>
+    <style include="gr-form-styles">
+      iron-autogrow-textarea {
+        width: 14em;
+      }
+    </style>
     <div id="mainContainer"
         class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
       <div id="options">
@@ -106,6 +110,22 @@
             </select>
           </gr-select>
         </template>
+        <template is="dom-if" if="[[hasRange]]">
+          <iron-autogrow-textarea
+              id="minInput"
+              class="min"
+              autocomplete="on"
+              placeholder="Min value"
+              bind-value="{{rule.value.min}}"
+              disabled$="[[!editing]]"></iron-autogrow-textarea>
+          <iron-autogrow-textarea
+              id="maxInput"
+              class="max"
+              autocomplete="on"
+              placeholder="Max value"
+              bind-value="{{rule.value.max}}"
+              disabled$="[[!editing]]"></iron-autogrow-textarea>
+        </template>
         <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
           [[groupName]]
         </a>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index b99125c..a45b391 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -65,8 +65,10 @@
 
   Polymer({
     is: 'gr-rule-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
+      hasRange: Boolean,
       /** @type {?} */
       label: Object,
       editing: {
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..ecc7532 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -26,6 +26,7 @@
 
   Polymer({
     is: 'gr-change-list-item',
+    _legacyUndefinedCheck: true,
 
     properties: {
       visibleChangeTableColumns: Array,
@@ -57,6 +58,9 @@
         type: String,
         computed: '_computeChangeSize(change)',
       },
+      _dynamicCellEndpoints: {
+        type: Array,
+      },
     },
 
     behaviors: [
@@ -67,6 +71,13 @@
       Gerrit.URLEncodingBehavior,
     ],
 
+    attached() {
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-list-item-cell');
+      });
+    },
+
     _computeItemNeedsReview(reviewed) {
       return !reviewed;
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index d5ae7c1..d546fe3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -31,6 +31,7 @@
 
   Polymer({
     is: 'gr-change-list-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
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 2235ae1..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,6 +52,13 @@
             [[_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">
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 708a730..2214d52 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -24,6 +24,7 @@
 
   Polymer({
     is: 'gr-change-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when next page key shortcut was pressed.
@@ -76,6 +77,9 @@
         type: Array,
         computed: '_computeLabelNames(sections)',
       },
+      _dynamicHeaderEndpoints: {
+        type: Array,
+      },
       selectedIndex: {
         type: Number,
         notify: true,
@@ -128,6 +132,13 @@
       };
     },
 
+    attached() {
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-list-header');
+      });
+    },
+
     /**
      * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
      * events must be scoped to a component level (e.g. `enter`) in order to not
@@ -226,6 +237,7 @@
 
     _computeItemNeedsReview(account, change, showReviewedState) {
       return showReviewedState && !change.reviewed &&
+          !change.work_in_progress &&
           this.changeIsOpen(change.status) &&
           (!account || account._account_id != change.owner._account_id);
     },
@@ -340,7 +352,9 @@
     },
 
     _getListItems() {
-      return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-change-list-item'));
     },
 
     _sectionsChanged() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index d20d40a..d5b9aa9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -231,11 +231,17 @@
           status: 'ABANDONED',
           owner: {_account_id: 0},
         },
+        {
+          _number: 4,
+          status: 'NEW',
+          work_in_progress: true,
+          owner: {_account_id: 0},
+        },
       ];
       flushAsynchronousOperations();
       let elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       for (let i = 0; i < elementItems.length; i++) {
         assert.isFalse(elementItems[i].hasAttribute('needs-review'));
       }
@@ -243,20 +249,22 @@
       element.showReviewedState = true;
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
 
       element.account = {_account_id: 42};
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
     });
 
     test('no changes', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index b5b02a7..42d7bd7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-create-change-help',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Create change" button is tapped.
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 0e71f1c..e4958c5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -25,6 +25,7 @@
 
   Polymer({
     is: 'gr-create-commands-dialog',
+    _legacyUndefinedCheck: true,
     properties: {
       branch: String,
       _createNewCommitCommand: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index 4d2802e..ed87e9e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -26,6 +26,7 @@
 
   Polymer({
     is: 'gr-create-destination-dialog',
+    _legacyUndefinedCheck: true,
     properties: {
       _repo: String,
       _branch: String,
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 0625bbe..d7ad0f4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-dashboard-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
index e31f9ad..14d0cb0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-embed-dashboard',
+    _legacyUndefinedCheck: true,
     properties: {
       account: Object,
       sections: Array,
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index cd6eb77..67fbd97 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-repo-header',
+    _legacyUndefinedCheck: true,
     properties: {
       /** @type {?String} */
       repo: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index cf5fefd..dc945d86 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-user-header',
+    _legacyUndefinedCheck: true,
     properties: {
       /** @type {?String} */
       userId: {
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index 715ddc0..fc0b633 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-entry',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when an account is entered.
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index 950c1e8..533ae09 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-account-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when user inputs an invalid email address.
@@ -88,7 +89,9 @@
     },
 
     get accountChips() {
-      return Polymer.dom(this.root).querySelectorAll('gr-account-chip');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-account-chip'));
     },
 
     get focusStart() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 6b6e90b..278875e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -32,6 +32,7 @@
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html">
 <link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
+<link rel="import" href="../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html">
 <link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
 <link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
@@ -187,6 +188,15 @@
           on-cancel="_handleConfirmDialogCancel"
           project="[[change.project]]"
           hidden></gr-confirm-cherrypick-dialog>
+      <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict"
+          class="confirmDialog"
+          change-status="[[changeStatus]]"
+          commit-message="[[commitMessage]]"
+          commit-num="[[commitNum]]"
+          on-confirm="_handleCherrypickConflictConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          project="[[change.project]]"
+          hidden></gr-confirm-cherrypick-conflict-dialog>
       <gr-confirm-move-dialog id="confirmMove"
           class="confirmDialog"
           on-confirm="_handleMoveConfirm"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 48591bf..ca0aa16 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -193,6 +193,7 @@
 
   Polymer({
     is: 'gr-change-actions',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change should be reloaded.
@@ -991,6 +992,14 @@
     },
 
     _handleCherrypickConfirm() {
+      this._handleCherryPickRestApi(false);
+    },
+
+    _handleCherrypickConflictConfirm() {
+      this._handleCherryPickRestApi(true);
+    },
+
+    _handleCherryPickRestApi(conflicts) {
       const el = this.$.confirmCherrypick;
       if (!el.branch) {
         // TODO(davido): Fix error handling
@@ -1009,7 +1018,9 @@
           true,
           {
             destination: el.branch,
+            base: el.baseCommit ? el.baseCommit : null,
             message: el.message,
+            allow_conflicts: conflicts,
           }
       );
     },
@@ -1112,8 +1123,9 @@
     _fireAction(endpoint, action, revAction, opt_payload) {
       const cleanupFn =
           this._setLoadingOnButtonWithKey(action.__type, action.__key);
-      this._send(action.method, opt_payload, endpoint, revAction, cleanupFn)
-          .then(this._handleResponse.bind(this, action));
+
+      this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
+          action).then(this._handleResponse.bind(this, action));
     },
 
     _showActionDialog(dialog) {
@@ -1170,7 +1182,14 @@
       });
     },
 
-    _handleResponseError(response) {
+    _handleResponseError(action, response, body) {
+      if (action && action.__key === RevisionActions.CHERRYPICK) {
+        if (response && response.status === 409 &&
+            body && !body.allow_conflicts) {
+          return this._showActionDialog(
+              this.$.confirmCherrypickConflict);
+        }
+      }
       return response.text().then(errText => {
         this.fire('show-error',
             {message: `Could not perform action: ${errText}`});
@@ -1186,13 +1205,12 @@
      * @param {string} actionEndpoint
      * @param {boolean} revisionAction
      * @param {?Function} cleanupFn
-     * @param {?Function=} opt_errorFn
+     * @param {!Object|undefined} action
      */
-    _send(method, payload, actionEndpoint, revisionAction, cleanupFn,
-        opt_errorFn) {
+    _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
       const handleError = response => {
         cleanupFn.call(this);
-        this._handleResponseError(response);
+        this._handleResponseError(action, response, payload);
       };
 
       return this.fetchChangeUpdates(this.change, this.$.restAPI)
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 89d4f1f..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({}); },
       });
@@ -595,7 +597,40 @@
         assert.deepEqual(fireActionStub.lastCall.args, [
           '/cherrypick', action, true, {
             destination: 'master',
+            base: null,
             message: 'foo message',
+            allow_conflicts: false,
+          },
+        ]);
+      });
+
+      test('cherry pick even with conflicts', () => {
+        element._handleCherrypickTap();
+        const action = {
+          __key: 'cherrypick',
+          __type: 'revision',
+          __primary: false,
+          enabled: true,
+          label: 'Cherry pick',
+          method: 'POST',
+          title: 'Cherry pick change to a different branch',
+        };
+
+        element.$.confirmCherrypick.branch = 'master';
+
+        // Add attributes that are used to determine the message.
+        element.$.confirmCherrypick.commitMessage = 'foo message';
+        element.$.confirmCherrypick.changeStatus = 'OPEN';
+        element.$.confirmCherrypick.commitNum = '123';
+
+        element._handleCherrypickConflictConfirm();
+
+        assert.deepEqual(fireActionStub.lastCall.args, [
+          '/cherrypick', action, true, {
+            destination: 'master',
+            base: null,
+            message: 'foo message',
+            allow_conflicts: true,
           },
         ]);
       });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index bec08a8..c07a775 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -330,11 +330,11 @@
             account="[[account]]"
             mutable="[[_mutable]]"></gr-change-requirements>
       </div>
-      <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo)]]">
+      <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo, serverConfig)]]">
         <span class="title">Links</span>
         <span class="value">
           <template is="dom-repeat"
-              items="[[_computeWebLinks(commitInfo)]]" as="link">
+              items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link">
             <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank">
               [[link.name]]
             </a>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index d3fc7e0..8ca8146 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -61,6 +61,7 @@
 
   Polymer({
     is: 'gr-change-metadata',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the change topic is changed.
@@ -191,12 +192,15 @@
      * an existential check can be used to hide or show the webLinks
      * section.
      */
-    _computeWebLinks(commitInfo) {
+    _computeWebLinks(commitInfo, serverConfig) {
       if (!commitInfo) { return null; }
       const weblinks = Gerrit.Nav.getChangeWeblinks(
           this.change ? this.change.repo : '',
           commitInfo.commit,
-          {weblinks: commitInfo.web_links});
+          {
+            weblinks: commitInfo.web_links,
+            config: serverConfig,
+          });
       return weblinks.length ? weblinks : null;
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index c5a569e..b23ac8d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -133,6 +133,7 @@
       const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
           .returns([{name: 'stubb', url: '#s'}]);
       element.commitInfo = {};
+      element.serverConfig = {};
       flushAsynchronousOperations();
       const webLinks = element.$.webLinks;
       assert.isTrue(weblinksStub.called);
@@ -142,6 +143,7 @@
 
     test('weblinks hidden when no weblinks', () => {
       element.commitInfo = {};
+      element.serverConfig = {};
       flushAsynchronousOperations();
       const webLinks = element.$.webLinks;
       assert.isTrue(webLinks.hasAttribute('hidden'));
@@ -149,12 +151,26 @@
 
     test('weblinks hidden when only gitiles weblink', () => {
       element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
+      element.serverConfig = {};
       flushAsynchronousOperations();
       const webLinks = element.$.webLinks;
       assert.isTrue(webLinks.hasAttribute('hidden'));
       assert.equal(element._computeWebLinks(element.commitInfo), null);
     });
 
+    test('weblinks hidden when sole weblink is set as primary', () => {
+      const browser = 'browser';
+      element.commitInfo = {web_links: [{name: browser, url: '#'}]};
+      element.serverConfig = {
+        gerrit: {
+          primary_weblink_name: browser,
+        },
+      };
+      flushAsynchronousOperations();
+      const webLinks = element.$.webLinks;
+      assert.isTrue(webLinks.hasAttribute('hidden'));
+    });
+
     test('weblinks are visible when other weblinks', () => {
       const router = document.createElement('gr-router');
       sandbox.stub(Gerrit.Nav, '_generateWeblinks',
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index 8ec00dd..09efa8d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-requirements',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 38f5f2f..e8158b8 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
@@ -24,7 +24,6 @@
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../edit/gr-edit-constants.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">
@@ -40,6 +39,7 @@
 <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-tooltip-content/gr-tooltip-content.html">
+<link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
@@ -200,6 +200,7 @@
       }
       .collapseToggleContainer {
         display: flex;
+        margin-bottom: 8px;
       }
       #relatedChangesToggle {
         display: none;
@@ -234,6 +235,7 @@
         --paper-tabs-selection-bar-color: var(--link-color);
       }
       paper-tab {
+        box-sizing: border-box;
         max-width: 15rem;
         --paper-tab-ink: var(--link-color);
       }
@@ -355,6 +357,9 @@
           min-width: initial;
           width: 100vw;
         }
+        #replyOverlay {
+          z-index: var(--reply-overlay-z-index);
+        }
       }
     </style>
     <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
@@ -486,6 +491,12 @@
                   [[_computeCollapseText(_commitCollapsed)]]
                 </gr-button>
               </div>
+              <gr-endpoint-decorator name="commit-container">
+                <gr-endpoint-param name="change" value="[[_change]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                </gr-endpoint-param>
+              </gr-endpoint-decorator>
             </div>
             <div class="relatedChanges">
               <gr-related-changes-list id="relatedChanges"
@@ -512,13 +523,33 @@
           </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]]"
             change="[[_change]]"
             change-num="[[_changeNum]]"
+            revision-info="[[_revisionInfo]]"
             change-comments="[[_changeComments]]"
             commit-info="[[_commitInfo]]"
             change-url="[[_computeChangeUrl(_change)]]"
@@ -532,6 +563,7 @@
             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"
@@ -559,7 +591,18 @@
             on-files-shown-changed="_setShownFiles"
             on-file-action-tap="_handleFileActionTap"
             on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
+        </div>
+
+        <template is="dom-if" if="[[!_showFileTabContent]]">
+            <gr-endpoint-decorator name$="[[_selectedFilesTabPluginEndpoint]]">
+                <gr-endpoint-param name="change" value="[[_change]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                </gr-endpoint-param>
+            </gr-endpoint-decorator>
+        </template>
       </section>
+
       <gr-endpoint-decorator name="change-view-integration">
         <gr-endpoint-param name="change" value="[[_change]]">
         </gr-endpoint-param>
@@ -568,7 +611,7 @@
       </gr-endpoint-decorator>
       <paper-tabs
           id="commentTabs"
-          on-selected-changed="_handleTabChange">
+          on-selected-changed="_handleCommentTabChange">
         <paper-tab class="changeLog">Change Log</paper-tab>
         <paper-tab
             class="commentThreads">
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 bb51fcc..bccd21e 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
@@ -64,6 +64,7 @@
 
   Polymer({
     is: 'gr-change-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -132,6 +133,7 @@
         type: Object,
         value: {},
       },
+      _prefs: Object,
       /** @type {?} */
       _changeComments: Object,
       _canStartReview: {
@@ -144,6 +146,10 @@
         type: Object,
         observer: '_changeChanged',
       },
+      _revisionInfo: {
+        type: Object,
+        computed: '_getRevisionInfo(_change)',
+      },
       /** @type {?} */
       _commitInfo: Object,
       _currentRevision: {
@@ -253,6 +259,25 @@
         type: Boolean,
         value: true,
       },
+      _showFileTabContent: {
+        type: Boolean,
+        value: true,
+      },
+      /** @type {Array<string>} */
+      _dynamicTabHeaderEndpoints: {
+        type: Array,
+      },
+      _showPrimaryTabs: {
+        type: Boolean,
+        computed: '_computeShowPrimaryTabs(_dynamicTabHeaderEndpoints)',
+      },
+      /** @type {Array<string>} */
+      _dynamicTabContentEndpoints: {
+        type: Array,
+      },
+      _selectedFilesTabPluginEndpoint: {
+        type: String,
+      },
     },
 
     behaviors: [
@@ -272,6 +297,7 @@
       'fullscreen-overlay-closed': '_handleShowBackgroundContent',
       'diff-comments-modified': '_handleReloadCommentThreads',
     },
+
     observers: [
       '_labelsChanged(_change.labels.*)',
       '_paramsAndChangeChanged(params, _change)',
@@ -310,6 +336,17 @@
         this._setDiffViewMode();
       });
 
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicTabHeaderEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
+        this._dynamicTabContentEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
+        if (this._dynamicTabContentEndpoints.length
+            !== this._dynamicTabHeaderEndpoints.length) {
+          console.warn('Different number of tab headers and tab content.');
+        }
+      });
+
       this.addEventListener('comment-save', this._handleCommentSave.bind(this));
       this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
       this.addEventListener('comment-discard',
@@ -345,7 +382,7 @@
     _setDiffViewMode(opt_reset) {
       if (!opt_reset && this.viewState.diffViewMode) { return; }
 
-      return this.$.restAPI.getPreferences().then( prefs => {
+      return this._getPreferences().then( prefs => {
         if (!this.viewState.diffMode) {
           this.set('viewState.diffMode', prefs.default_diff_view);
         }
@@ -368,10 +405,18 @@
       }
     },
 
-    _handleTabChange() {
+    _handleCommentTabChange() {
       this._showMessagesView = this.$.commentTabs.selected === 0;
     },
 
+    _handleFileTabChange() {
+      const selectedIndex = this.$$('#primaryTabs').selected;
+      this._showFileTabContent = selectedIndex === 0;
+      // Initial tab is the static files list.
+      this._selectedFilesTabPluginEndpoint =
+          this._dynamicTabContentEndpoints[selectedIndex - 1];
+    },
+
     _handleEditCommitMessage(e) {
       this._editingCommitMessage = true;
       this.$.commitMessageEditor.focusTextarea();
@@ -699,6 +744,8 @@
       // Selected has to be set after the paper-tabs are visible because
       // the selected underline depends on calculations made by the browser.
       this.$.commentTabs.selected = 0;
+      const primaryTabs = this.$$('#primaryTabs');
+      if (primaryTabs) primaryTabs.selected = 0;
 
       this.async(() => {
         if (this.viewState.scrollTop) {
@@ -807,10 +854,11 @@
 
     _changeChanged(change) {
       if (!change || !this._patchRange || !this._allPatchSets) { return; }
-      this.set('_patchRange.basePatchNum',
-          this._patchRange.basePatchNum || 'PARENT');
-      this.set('_patchRange.patchNum',
-          this._patchRange.patchNum ||
+
+      const parent = this._getBasePatchNum(change, this._patchRange);
+
+      this.set('_patchRange.basePatchNum', parent);
+      this.set('_patchRange.patchNum', this._patchRange.patchNum ||
               this.computeLatestPatchNum(this._allPatchSets));
 
       // Reset the related changes toggle in the event it was previously
@@ -821,6 +869,39 @@
       this.fire('title-change', {title});
     },
 
+    /**
+     * Gets base patch number, if is a parent try and
+     * decide from preference weather to default to `auto merge`
+     * or `Parent 1`.
+     * @param {Object} change
+     * @param {Object} patchRange
+     * @return {number|string}
+     */
+    _getBasePatchNum(change, patchRange) {
+      if (patchRange.basePatchNum &&
+          patchRange.basePatchNum !== 'PARENT') {
+        return patchRange.basePatchNum;
+      }
+
+      const revisionInfo = this._getRevisionInfo(change);
+      if (!revisionInfo) return 'PARENT';
+
+      const parentCounts = revisionInfo.getParentCountMap();
+      // check that there is at least 2 parents otherwise fall back to 1,
+      // which means there is only one parent.
+      const parentCount = parentCounts.hasOwnProperty(1) ?
+          parentCounts[1] : 1;
+
+      const preferFirst = this._prefs &&
+          this._prefs.default_base_for_merges === 'FIRST_PARENT';
+
+      return parentCount > 1 && preferFirst ? -1 : 'PARENT';
+    },
+
+    _computeShowPrimaryTabs(dynamicTabContentEndpoints) {
+      return dynamicTabContentEndpoints.length > 0;
+    },
+
     _computeChangeUrl(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
@@ -1076,6 +1157,10 @@
           });
     },
 
+    _getPreferences() {
+      return this.$.restAPI.getPreferences();
+    },
+
     _updateRebaseAction(revisionActions) {
       if (revisionActions && revisionActions.rebase) {
         revisionActions.rebase.rebaseOnCurrent =
@@ -1129,9 +1214,12 @@
       const detailCompletes = this.$.restAPI.getChangeDetail(
           this._changeNum, this._handleGetChangeDetailError.bind(this));
       const editCompletes = this._getEdit();
+      const prefCompletes = this._getPreferences();
 
-      return Promise.all([detailCompletes, editCompletes])
-          .then(([change, edit]) => {
+      return Promise.all([detailCompletes, editCompletes, prefCompletes])
+          .then(([change, edit, prefs]) => {
+            this._prefs = prefs;
+
             if (!change) {
               return '';
             }
@@ -1680,6 +1768,10 @@
           e.detail.starred);
     },
 
+    _getRevisionInfo(change) {
+      return new Gerrit.RevisionInfo(change);
+    },
+
     _computeCurrentRevision(currentRevision, revisions) {
       return revisions && revisions[currentRevision];
     },
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 a88142e..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
@@ -297,7 +297,8 @@
       });
 
       test(', should open diff preferences', () => {
-        const stub = sandbox.stub(element.$.fileList.$.diffPreferences, 'open');
+        const stub = sandbox.stub(
+            element.$.fileList.$.diffPreferencesDialog, 'open');
         element._loggedIn = false;
         element.disableDiffPrefs = true;
         MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
@@ -1604,8 +1605,8 @@
     });
 
     test('_selectedRevision updates when patchNum is changed', () => {
-      const revision1 = {_number: 1, commit: {}};
-      const revision2 = {_number: 2, commit: {}};
+      const revision1 = {_number: 1, commit: {parents: []}};
+      const revision2 = {_number: 2, commit: {parents: []}};
       sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
           Promise.resolve({
             revisions: {
@@ -1618,6 +1619,7 @@
             change_id: 'loremipsumdolorsitamet',
           }));
       sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+      sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
       element._patchRange = {patchNum: '2'};
       return element._getChangeDetail().then(() => {
         assert.strictEqual(element._selectedRevision, revision2);
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index cbc7e42..660dfd9 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -18,6 +18,7 @@
   'use strict';
   Polymer({
     is: 'gr-comment-list',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index 837de59..ddab319 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-commit-info',
+    _legacyUndefinedCheck: true,
 
     properties: {
       change: Object,
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index 234d903..438a3f3 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -99,57 +99,6 @@
           element.serverConfig), 'https://link-url');
     });
 
-    test('use gitweb when available', () => {
-      const router = document.createElement('gr-router');
-      element.serverConfig = {gitweb: {
-        url: 'url-base/',
-        type: {revision: 'xx ${project} xx ${commit} xx'},
-      }};
-      sandbox.stub(Gerrit.Nav, '_generateWeblinks',
-          router._generateWeblinks.bind(router));
-
-      element.commitInfo = {commit: 'commit-sha'};
-      element.change = {
-        project: 'project-name',
-        labels: [],
-        current_revision: element.commitInfo.commit,
-      };
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-
-      assert.equal(element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig), 'url-base/xx project-name xx commit-sha xx');
-    });
-
-    test('prefer gitweb when both are available', () => {
-      const router = document.createElement('gr-router');
-      element.serverConfig = {gitweb: {
-        url: 'url-base/',
-        type: {revision: 'xx ${project} xx ${commit} xx'},
-      }};
-      sandbox.stub(Gerrit.Nav, '_generateWeblinks',
-          router._generateWeblinks.bind(router));
-
-      element.commitInfo = {
-        commit: 'commit-sha',
-        web_links: [{url: 'link-url'}],
-      };
-      element.change = {
-        project: 'project-name',
-        labels: [],
-        current_revision: element.commitInfo.commit,
-      };
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-
-      const link = element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig);
-
-      assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
-      assert.notEqual(link, '../../link-url');
-    });
 
     test('ignore web links that are neither gitweb nor gitiles', () => {
       const router = document.createElement('gr-router');
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index 03509ce..a371e13 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-abandon-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
new file mode 100644
index 0000000..cd196ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
@@ -0,0 +1,51 @@
+<!--
+@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="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
+
+<dom-module id="gr-confirm-cherrypick-conflict-dialog">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      .main {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+      }
+    </style>
+    <gr-dialog
+        confirm-label="Continue"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header" slot="header">Cherry Pick Conflict!</div>
+      <div class="main" slot="main">
+        <span>Cherry Pick failed! (merge conflicts)</span>
+
+        <span>Please select "Continue" to continue with conflicts or select "cancel" to close the dialog.</span>
+      </div>
+    </gr-dialog>
+  </template>
+  <script src="gr-confirm-cherrypick-conflict-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
new file mode 100644
index 0000000..2e8af0e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -0,0 +1,46 @@
+/**
+ * @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-confirm-cherrypick-conflict-dialog',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    _handleConfirmTap(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
new file mode 100644
index 0000000..77b102c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-cherrypick-conflict-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-cherrypick-conflict-dialog></gr-confirm-cherrypick-conflict-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('_handleConfirmTap', () => {
+      const confirmHandler = sandbox.stub();
+      element.addEventListener('confirm', confirmHandler);
+      sandbox.stub(element, '_handleConfirmTap');
+      element.$$('gr-dialog').fire('confirm');
+      assert.isTrue(confirmHandler.called);
+      assert.isTrue(element._handleConfirmTap.called);
+    });
+
+    test('_handleCancelTap', () => {
+      const cancelHandler = sandbox.stub();
+      element.addEventListener('cancel', cancelHandler);
+      sandbox.stub(element, '_handleCancelTap');
+      element.$$('gr-dialog').fire('cancel');
+      assert.isTrue(cancelHandler.called);
+      assert.isTrue(element._handleCancelTap.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
index 9e0aa8c..84558ac 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -74,6 +74,15 @@
             query="[[_query]]"
             placeholder="Destination branch">
         </gr-autocomplete>
+        <label for="baseInput">
+          Provide base commit sha1 for cherry-pick
+        </label>
+        <input
+            is="iron-input"
+            id="baseCommitInput"
+            maxlength="40"
+            placeholder="(optional)"
+            bind-value="{{baseCommit}}">
         <label for="messageInput">
           Cherry Pick Commit Message
         </label>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index ea63dd5..fa281ec 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-confirm-cherrypick-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -36,6 +37,7 @@
 
     properties: {
       branch: String,
+      baseCommit: String,
       changeStatus: String,
       commitMessage: String,
       commitNum: String,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index f8d7151..093dca6 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-confirm-move-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 29f8b95..1c3bcfb 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-rebase-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 1cae866..93e21c7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-confirm-revert-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
index 346bdd0..1036b7f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
@@ -49,9 +49,10 @@
       </div>
       <div class="main" slot="main">
         <gr-endpoint-decorator name="confirm-submit-change">
-          <p>
-            Ready to submit &ldquo;<strong>[[change.subject]]</strong>&rdquo;?
-          </p>
+          <p>Ready to submit &ldquo;<strong>[[change.subject]]</strong>&rdquo;?</p>
+          <template is="dom-if" if="[[change.is_private]]">
+            <p><strong>Heads Up!</strong> Submitting this private change will also make it public.</p>
+          </template>
           <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
           <gr-endpoint-param name="action" value="[[action]]"></gr-endpoint-param>
         </gr-endpoint-decorator>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index bda79a1..93d38df 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-submit-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
@@ -35,6 +36,7 @@
     properties: {
       /**
        * @type {{
+       *    is_private: boolean,
        *    subject: string,
        *  }}
        */
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 5e82cda..28d25d2 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -19,8 +19,8 @@
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
 
 <dom-module id="gr-download-dialog">
   <template>
@@ -87,7 +87,7 @@
           selected-scheme="{{_selectedScheme}}"></gr-download-commands>
     </section>
     <section class="flexContainer">
-      <div class="patchFiles">
+      <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]" hidden>
         <label>Patch file</label>
         <div>
           <a
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 5fc81e8..9bfe3a9 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-download-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
@@ -138,6 +139,17 @@
       return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
     },
 
+    _computeHidePatchFile(change, patchNum) {
+      for (const rev of Object.values(change.revisions || {})) {
+        if (this.patchNumEquals(rev._number, patchNum)) {
+          const parentLength = rev.commit && rev.commit.parents ?
+                rev.commit.parents.length : 0;
+          return parentLength == 0;
+        }
+      }
+      return false;
+    },
+
     _computeArchiveDownloadLink(change, patchNum, format) {
       return this.changeBaseURL(change.project, change._number, patchNum) +
           '/archive?format=' + format;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 2915e29..ee284b9 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -45,6 +45,9 @@
       revisions: {
         '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
           _number: 1,
+          commit: {
+            parents: [],
+          },
           fetch: {
             repo: {
               commands: {
@@ -105,6 +108,9 @@
       revisions: {
         '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
           _number: 1,
+          commit: {
+            parents: [],
+          },
           fetch: {},
         },
       },
@@ -188,5 +194,25 @@
       assert.equal(element._computeShowDownloadCommands([]), 'hidden');
       assert.equal(element._computeShowDownloadCommands(['test']), '');
     });
+
+    test('_computeHidePatchFile', () => {
+      const patchNum = '1';
+
+      const change1 = {
+        revisions: {
+          r1: {_number: 1, commit: {parents: []}},
+        },
+      };
+      assert.isTrue(element._computeHidePatchFile(change1, patchNum));
+
+      const change2 = {
+        revisions: {
+          r1: {_number: 1, commit: {parents: [
+            {commit: 'p1'},
+          ]}},
+        },
+      };
+      assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+    });
   });
 </script>
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 924ddab..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
@@ -28,7 +28,6 @@
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-file-list-constants.html">
 
 <dom-module id="gr-file-list-header">
@@ -154,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"
@@ -164,7 +166,7 @@
               base-patch-num="[[basePatchNum]]"
               available-patches="[[allPatchSets]]"
               revisions="[[change.revisions]]"
-              revision-info="[[_revisionInfo]]"
+              revision-info="[[revisionInfo]]"
               on-patch-range-change="_handlePatchChange">
           </gr-patch-range-select>
           <span class="separator"></span>
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 b9e6288..250900a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -23,6 +23,7 @@
 
   Polymer({
     is: 'gr-file-list-header',
+    _legacyUndefinedCheck: true,
 
     /**
      * @event expand-diffs
@@ -81,14 +82,15 @@
         type: String,
         value: '',
       },
+      showTitle: {
+        type: Boolean,
+        value: true,
+      },
       _descriptionReadOnly: {
         type: Boolean,
         computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
       },
-      _revisionInfo: {
-        type: Object,
-        computed: '_getRevisionInfo(change)',
-      },
+      revisionInfo: Object,
     },
 
     behaviors: [
@@ -230,10 +232,6 @@
       return 'patchInfoOldPatchSet';
     },
 
-    _getRevisionInfo(change) {
-      return new Gerrit.RevisionInfo(change);
-    },
-
     _hideIncludedIn(change) {
       return change && change.status === MERGED_STATUS ? '' : 'hide';
     },
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 fa9ec7f..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
@@ -23,8 +23,9 @@
 <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="../../diff/gr-diff-host/gr-diff-host.html">
 <link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html">
+<link rel="import" href="../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
 <link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
@@ -364,7 +365,7 @@
               <span class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
               <label>
                 <input class="reviewed" type="checkbox" checked="[[file.isReviewed]]">
-                <span class="markReviewed" title="Mark as reviewed (shortcut: r)">[[_computeReviewedText(file.isReviewed)]]</span>
+                <span class="markReviewed" title$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span>
               </label>
             </div>
             <div class="editFileControls showOnEdit">
@@ -464,10 +465,11 @@
         </gr-button><!--
   --></gr-tooltip-content>
     </div>
-    <gr-diff-preferences
-        id="diffPreferences"
-        prefs="{{diffPrefs}}"
-        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
+    <gr-diff-preferences-dialog
+        id="diffPreferencesDialog"
+        diff-prefs="{{diffPrefs}}"
+        on-reload-diff-preference="_handleReloadingDiffPreference">
+    </gr-diff-preferences-dialog>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
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 67fdff1..9d2710b2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -63,6 +63,7 @@
 
   Polymer({
     is: 'gr-file-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a draft refresh should get triggered
@@ -125,7 +126,6 @@
       },
       /** @type {?} */
       _userPrefs: Object,
-      _localPrefs: Object,
       _showInlineDiffs: Boolean,
       numFilesShown: {
         type: Number,
@@ -269,7 +269,6 @@
         });
       }));
 
-      this._localPrefs = this.$.storage.getPreferences();
       promises.push(this._getDiffPreferences().then(prefs => {
         this.diffPrefs = prefs;
       }));
@@ -293,11 +292,13 @@
     },
 
     get diffs() {
-      return Polymer.dom(this.root).querySelectorAll('gr-diff-host');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('gr-diff-host'));
     },
 
     openDiffPrefs() {
-      this.$.diffPreferences.open();
+      this.$.diffPreferencesDialog.open();
     },
 
     _calculatePatchChange(files) {
@@ -860,7 +861,9 @@
 
     _filesChanged() {
       Polymer.dom.flush();
-      const files = Polymer.dom(this.root).querySelectorAll('.file-row');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      const files = Array.from(
+          Polymer.dom(this.root).querySelectorAll('.file-row'));
       this.$.fileCursor.stops = files;
       this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
     },
@@ -1247,5 +1250,19 @@
       }
       return '';
     },
+
+    _reviewedTitle(reviewed) {
+      if (reviewed) {
+        return 'Mark as not reviewed (shortcut: r)';
+      }
+
+      return 'Mark as reviewed (shortcut: r)';
+    },
+
+    _handleReloadingDiffPreference() {
+      this._getDiffPreferences().then(prefs => {
+        this.diffPrefs = prefs;
+      });
+    },
   });
 })();
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 51f0e5f..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
@@ -552,6 +552,14 @@
               'unresolved.file', 'comment'), '3 comments (1 unresolved)');
     });
 
+    test('_reviewedTitle', () => {
+      assert.equal(
+          element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)');
+
+      assert.equal(
+          element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)');
+    });
+
     suite('keyboard shortcuts', () => {
       setup(() => {
         element._filesByPath = {
@@ -1402,6 +1410,7 @@
         ignore_whitespace: 'IGNORE_NONE',
       };
       diff._diff = mock.diffResponse;
+      diff.$.diff.flushDebouncer('renderDiffTable');
     };
 
     const renderAndGetNewDiffs = function(index) {
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index d2ff035..7755a60 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-included-in-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 0ac5019..27c5baa 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -61,7 +61,7 @@
           background-color: var(--button-background-color, var(--table-header-background-color));
           color: var(--primary-text-color);
           padding: .2em .85em;
-          @apply(--vote-chip-styles);
+          @apply --vote-chip-styles;
         }
       }
       gr-button.iron-selected.max {
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index f472331..d9b961f 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-score-row',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when any label is changed.
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index 8734da2..eaf39bc 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-scores',
+    _legacyUndefinedCheck: true,
     properties: {
       _labels: {
         type: Array,
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 8ecd6b0..837ffe0 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-message',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when this message's reply link is tapped.
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 023431c..81c3322 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -27,6 +27,7 @@
 
   Polymer({
     is: 'gr-messages-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       changeNum: Number,
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 39f4d8d..8a8b6b3 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-related-changes-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a new section is loaded so that the change view can determine
@@ -190,7 +191,8 @@
     },
 
     _getChangesWithSameTopic() {
-      return this.$.restAPI.getChangesWithSameTopic(this.change.topic);
+      return this.$.restAPI.getChangesWithSameTopic(this.change.topic,
+          this.change._number);
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 9f9f026..8ca9128 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -58,6 +58,7 @@
 
   Polymer({
     is: 'gr-reply-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a reply is successfully sent.
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index ab1f55e..1cae50a 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-reviewer-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the "Add reviewer..." button is tapped.
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index 69d77f6..8f15a06 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -25,6 +25,7 @@
 
   Polymer({
     is: 'gr-thread-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {?} */
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 02d00cf..df96be2 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -29,6 +29,7 @@
 
   Polymer({
     is: 'gr-upload-help-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 72ec7fa..af4510d 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-account-dropdown',
+    _legacyUndefinedCheck: true,
 
     properties: {
       account: Object,
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 8d3b58e..5679408 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-error-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dismiss button is pressed.
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index 3ec4bb5..4ca106e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-alert/gr-alert.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -32,6 +33,7 @@
           confirm-on-enter></gr-error-dialog>
     </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-error-manager.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index b254182..38dfecb 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -27,6 +27,7 @@
 
   Polymer({
     is: 'gr-error-manager',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
@@ -283,6 +284,7 @@
     },
 
     _showErrorDialog(message) {
+      this.$.reporting.reportErrorDialog(message);
       this.$.errorDialog.text = message;
       this.$.errorOverlay.open();
     },
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index f92feae..88e3efd 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -304,11 +304,14 @@
     test('show-error', () => {
       const openStub = sandbox.stub(element.$.errorOverlay, 'open');
       const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
+      const reportStub = sandbox.stub(element.$.reporting, 'reportErrorDialog');
+
       const message = 'test message';
       element.fire('show-error', {message});
       flushAsynchronousOperations();
 
       assert.isTrue(openStub.called);
+      assert.isTrue(reportStub.called);
       assert.equal(element.$.errorDialog.text, message);
 
       element.$.errorDialog.fire('dismiss');
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 89d1091..e8c6479 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-key-binding-display',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {Array<string>} */
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index 5b29972..f206db1 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-keyboard-shortcuts-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user presses the close button.
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index a77c66d..1f2c720 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -71,6 +71,7 @@
 
   Polymer({
     is: 'gr-main-header',
+    _legacyUndefinedCheck: true,
 
     hostAttributes: {
       role: 'banner',
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..2cee55c 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -69,6 +69,11 @@
     CATEGORY: 'exception',
   };
 
+  const ERROR_DIALOG = {
+    TYPE: 'error',
+    CATEGORY: 'Error Dialog',
+  };
+
   const TIMER = {
     CHANGE_DISPLAYED: 'ChangeDisplayed',
     CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
@@ -139,6 +144,7 @@
   // eslint-disable-next-line prefer-const
   let GrReporting = Polymer({
     is: 'gr-reporting',
+    _legacyUndefinedCheck: true,
 
     properties: {
       category: String,
@@ -191,7 +197,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 +216,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 +235,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 +456,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 8ae30ad..e6ad03e 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -204,6 +204,7 @@
 
   Polymer({
     is: 'gr-router',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _app: {
@@ -280,55 +281,53 @@
     },
 
     _getPatchSetWeblink(params) {
-      const {repo, commit, options} = params;
+      const {commit, options} = params;
       const {weblinks, config} = options || {};
       const name = commit && commit.slice(0, 7);
-      const gitwebConfigUrl = this._configBasedCommitUrl(repo, commit, config);
-      if (gitwebConfigUrl) {
-        return {
-          name,
-          url: gitwebConfigUrl,
-        };
-      }
-      const url = this._getSupportedWeblinkUrl(weblinks);
-      if (!url) {
+      const weblink = this._getBrowseCommitWeblink(weblinks, config);
+      if (!weblink || !weblink.url) {
         return {name};
       } else {
-        return {name, url};
+        return {name, url: weblink.url};
       }
     },
 
-    _configBasedCommitUrl(repo, commit, config) {
-      if (config && config.gitweb && config.gitweb.url &&
-          config.gitweb.type && config.gitweb.type.revision) {
-        return config.gitweb.url + config.gitweb.type.revision
-            .replace('${project}', repo)
-            .replace('${commit}', commit);
+    _firstCodeBrowserWeblink(weblinks) {
+      // This is an ordered whitelist of web link types that provide direct
+      // links to the commit in the url property.
+      const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
+      for (let i = 0; i < codeBrowserLinks.length; i++) {
+        const weblink =
+          weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
+        if (weblink) { return weblink; }
       }
+      return null;
     },
 
-    _isDirectCommit(link) {
-      // This is a whitelist of web link types that provide direct links to
-      // the commit in the url property.
-      return link.name === 'gitiles' || link.name === 'gitweb';
-    },
 
-    _getSupportedWeblinkUrl(weblinks) {
+    _getBrowseCommitWeblink(weblinks, config) {
       if (!weblinks) { return null; }
-      const weblink = weblinks.find(this._isDirectCommit);
+      let weblink;
+      // Use primary weblink if configured and exists.
+      if (config && config.gerrit && config.gerrit.primary_weblink_name) {
+        weblink = weblinks.find(
+            weblink => weblink.name === config.gerrit.primary_weblink_name
+        );
+      }
+      if (!weblink) {
+        weblink = this._firstCodeBrowserWeblink(weblinks);
+      }
       if (!weblink) { return null; }
-      return weblink.url;
+      return weblink;
     },
 
-    _getChangeWeblinks({repo, commit, options: {weblinks}}) {
+    _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
       if (!weblinks || !weblinks.length) return [];
-      return weblinks.filter(weblink => !this._isDirectCommit(weblink)).map(
-          ({name, url}) => {
-            if (!url.startsWith('https:') && !url.startsWith('http:')) {
-              url = this.getBaseUrl() + (url.startsWith('/') ? '' : '/') + url;
-            }
-            return {name, url};
-          });
+      const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
+      return weblinks.filter(weblink =>
+        !commitWeblink ||
+        !commitWeblink.name ||
+        weblink.name !== commitWeblink.name);
     },
 
     _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
@@ -638,7 +637,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 584fb35..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
@@ -44,20 +44,46 @@
 
     teardown(() => { sandbox.restore(); });
 
-    test('_getChangeWeblinks', () => {
-      sandbox.stub(element, '_isDirectCommit').returns(false);
-      sandbox.stub(element, 'getBaseUrl').returns('base');
+    test('_firstCodeBrowserWeblink', () => {
+      assert.deepEqual(element._firstCodeBrowserWeblink([
+        {name: 'gitweb'},
+        {name: 'gitiles'},
+        {name: 'browse'},
+        {name: 'test'}]), {name: 'gitiles'});
+
+      assert.deepEqual(element._firstCodeBrowserWeblink([
+        {name: 'gitweb'},
+        {name: 'test'}]), {name: 'gitweb'});
+    });
+
+    test('_getBrowseCommitWeblink', () => {
+      const browserLink = {name: 'browser', url: 'browser/url'};
       const link = {name: 'test', url: 'test/url'};
-      const mapLinksToConfig = weblink => ({options: {weblinks: [weblink]}});
-      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig(link))[0],
-          {name: 'test', url: 'base/test/url'});
+      const weblinks = [browserLink, link];
+      const config = {gerrit: {primary_weblink_name: browserLink.name}};
+      sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link);
 
-      link.url = '/' + link.url;
-      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig(link))[0],
-          {name: 'test', url: 'base/test/url'});
+      assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
+          browserLink);
 
-      link.url = 'https:/' + link.url;
-      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig(link))[0],
+      assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
+    });
+
+    test('_getChangeWeblinks', () => {
+      const link = {name: 'test', url: 'test/url'};
+      const browserLink = {name: 'browser', url: 'browser/url'};
+      const mapLinksToConfig = weblinks => ({options: {weblinks}});
+      sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
+
+      assert.deepEqual(
+          element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
+          {name: 'test', url: 'test/url'});
+
+      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
+          {name: 'test', url: 'test/url'});
+
+      link.url = 'https://' + link.url;
+      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
           {name: 'test', url: 'https://test/url'});
     });
 
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 a81526c..e37fb2e 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',
@@ -60,10 +65,13 @@
     'is:reviewed',
     'is:reviewer',
     'is:starred',
+    'is:submittable',
     'is:watched',
     'is:wip',
     'label:',
     'message:',
+    'onlyexts:',
+    'onlyextensions:',
     'owner:',
     'ownerin:',
     'parentproject:',
@@ -98,6 +106,7 @@
 
   Polymer({
     is: 'gr-search-bar',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a search is committed
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index a921308..fed02d6 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -23,6 +23,7 @@
 
   Polymer({
     is: 'gr-smart-search',
+    _legacyUndefinedCheck: true,
 
     properties: {
       searchQuery: String,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
index b7994e6..7bf71f5 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'comment-api-mock',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 4b64f7b..b5ff9a3 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -482,6 +482,7 @@
 
   Polymer({
     is: 'gr-comment-api',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _changeComments: Object,
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 2cf9782..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
@@ -89,14 +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 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 6020e19..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
@@ -68,24 +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 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 42a262a..b5f21b6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -15,7 +15,6 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../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">
@@ -35,9 +34,9 @@
     <gr-diff-processor
         id="processor"
         groups="{{_groups}}"></gr-diff-processor>
-    <gr-reporting id="reporting"></gr-reporting>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   </template>
+  <script src="../../../scripts/util.js"></script>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff/gr-diff-group.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
@@ -55,12 +54,6 @@
         UNIFIED: 'UNIFIED_DIFF',
       };
 
-      const TimingLabel = {
-        TOTAL: 'Diff Total Render',
-        CONTENT: 'Diff Content Render',
-        SYNTAX: 'Diff Syntax Render',
-      };
-
       // If any line of the diff is more than the character limit, then disable
       // syntax highlighting for the entire file.
       const SYNTAX_MAX_LINE_LENGTH = 500;
@@ -72,6 +65,7 @@
 
       Polymer({
         is: 'gr-diff-builder',
+        _legacyUndefinedCheck: true,
 
         /**
          * Fired when the diff begins rendering.
@@ -80,16 +74,16 @@
          */
 
         /**
-         * Fired when the diff is rendered.
+         * Fired when the diff finishes rendering text content and starts
+         * syntax highlighting.
          *
-         * @event render
+         * @event render-content
          */
 
         /**
-         * Fired when the diff finishes rendering text content, but not
-         * necessarily syntax highlights.
+         * Fired when the diff finishes syntax highlighting.
          *
-         * @event render-content
+         * @event render-syntax
          */
 
         properties: {
@@ -114,6 +108,13 @@
             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() {
@@ -146,33 +147,34 @@
           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}));
+          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;
-                }
+                    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}));
-                });
-              });
+                    return this.$.syntaxLayer.process();
+                  })
+                  .then(() => {
+                    this.dispatchEvent(
+                        new CustomEvent('render-syntax', {bubbles: true}));
+                  }));
+          return this._cancelableRenderPromise
+              .finally(() => { this._cancelableRenderPromise = null; })
+              // Mocca testing does not like uncaught rejections, so we catch
+              // the cancels which are expected and should not throw errors in
+              // tests.
+              .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
         },
 
         _setupAnnotationLayers() {
@@ -267,6 +269,10 @@
         cancel() {
           this.$.processor.cancel();
           this.$.syntaxLayer.cancel();
+          if (this._cancelableRenderPromise) {
+            this._cancelableRenderPromise.cancel();
+            this._cancelableRenderPromise = null;
+          }
         },
 
         _handlePreferenceError(pref) {
@@ -331,7 +337,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
@@ -345,7 +351,7 @@
                     highlight.endIndex;
 
                 GrAnnotation.annotateElement(
-                    el,
+                    contentEl,
                     highlight.startIndex,
                     endIndex - highlight.startIndex,
                     HL_CLASS);
@@ -357,7 +363,7 @@
         _createTabIndicatorLayer() {
           const show = () => this._showTabs;
           return {
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               // If visible tabs are disabled, do nothing.
               if (!show()) { return; }
 
@@ -368,7 +374,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.
@@ -384,7 +390,7 @@
           }.bind(this);
 
           return {
-            annotate(el, line) {
+            annotate(contentEl, lineNumberEl, line) {
               if (!show()) { return; }
 
               const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
@@ -394,7 +400,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');
               }
             },
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 e892605..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
@@ -218,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);
     }
   };
@@ -343,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');
@@ -360,7 +363,7 @@
     }
 
     for (const layer of this.layers) {
-      layer.annotate(contentText, line);
+      layer.annotate(contentText, lineNumberEl, line);
     }
 
     td.appendChild(contentText);
@@ -594,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 294d085..481db57 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
@@ -163,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);
     });
 
@@ -173,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.
@@ -306,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('');
@@ -325,7 +326,7 @@
           highlights: [],
         };
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         // The content is unchanged.
         assert.isFalse(annotateElementSpy.called);
@@ -348,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);
@@ -380,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);
@@ -400,7 +401,7 @@
           ],
         };
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementSpy.called);
         assert.equal(el.childNodes.length, 1);
@@ -421,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);
@@ -451,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);
@@ -467,6 +468,7 @@
     suite('tab indicators', () => {
       let element;
       let layer;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         element = fixture('basic');
@@ -480,7 +482,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -493,7 +495,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -506,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;
@@ -526,7 +528,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -539,7 +541,7 @@
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
 
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
 
         assert.equal(annotateElementStub.callCount, 2);
 
@@ -564,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;
@@ -606,6 +608,7 @@
     suite('trailing whitespace', () => {
       let element;
       let layer;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         element = fixture('basic');
@@ -618,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);
       });
 
@@ -629,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);
       });
 
@@ -640,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);
@@ -653,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);
@@ -666,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);
@@ -679,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);
@@ -693,7 +696,7 @@
         el.textContent = str;
         const annotateElementStub =
             sandbox.stub(GrAnnotation, 'annotateElement');
-        layer.annotate(el, line);
+        layer.annotate(el, lineNumberEl, line);
         assert.isFalse(annotateElementStub.called);
       });
     });
@@ -780,10 +783,6 @@
             ],
           },
         ];
-        stub('gr-reporting', {
-          time: sandbox.stub(),
-          timeEnd: sandbox.stub(),
-        });
         element = fixture('basic');
         outputEl = element.queryEffectiveChildren('#diffTable');
         keyLocations = {left: {}, right: {}};
@@ -803,18 +802,6 @@
         element.render(keyLocations, prefs).then(done);
       });
 
-      test('reporting', done => {
-        const timeStub = element.$.reporting.time;
-        const timeEndStub = element.$.reporting.timeEnd;
-        assert.isTrue(timeStub.calledWithExactly('Diff Total Render'));
-        assert.isTrue(timeStub.calledWithExactly('Diff Content Render'));
-        assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render'));
-        assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render'));
-        done();
-      });
-
       test('renderSection', () => {
         let section = outputEl.querySelector('stub:nth-of-type(2)');
         const prevInnerHTML = section.innerHTML;
@@ -847,14 +834,14 @@
         assert.strictEqual(sections[1], section[1]);
       });
 
-      test('render-start and render are fired', done => {
+      test('render-start and render-content are fired', done => {
         const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
         element.render(keyLocations, {}).then(() => {
           const firedEventTypes = dispatchEventStub.getCalls()
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
           assert.include(firedEventTypes, 'render-content');
-          assert.include(firedEventTypes, 'render');
+          assert.include(firedEventTypes, 'render-syntax');
           done();
         });
       });
@@ -866,10 +853,6 @@
       test('rendering large diff disables syntax', done => {
         // Before it renders, set the first diff line to 500 '*' characters.
         element.diff.content[0].a = [new Array(501).join('*')];
-        element.addEventListener('render', () => {
-          assert.isFalse(element.$.syntaxLayer.enabled);
-          done();
-        });
         const prefs = {
           line_length: 10,
           show_tabs: true,
@@ -877,7 +860,10 @@
           context: -1,
           syntax_highlighting: true,
         };
-        element.render(keyLocations, prefs);
+        element.render(keyLocations, prefs).then(() => {
+          assert.isFalse(element.$.syntaxLayer.enabled);
+          done();
+        });
       });
 
       test('cancel', () => {
@@ -964,7 +950,7 @@
 
         assert.equal(spy.callCount, count);
         spy.getCalls().forEach((call, i) => {
-          assert.equal(call.args[0].beforeNumber, start + i);
+          assert.equal(call.args[1].beforeNumber, start + i);
         });
       });
 
@@ -975,9 +961,11 @@
             (s, e, d, lines, elements) => {
               // Add a line and a corresponding element.
               lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
-              const parEl = document.createElement('div');
+              const tr = document.createElement('tr');
+              const td = document.createElement('td');
               const el = document.createElement('div');
-              parEl.appendChild(el);
+              tr.appendChild(td);
+              td.appendChild(el);
               elements.push(el);
 
               // Add 2 lines without corresponding elements.
@@ -991,6 +979,52 @@
         assert.equal(spy.callCount, 1);
       });
 
+      test('_getLineNumberEl side-by-side left', () => {
+        const contentEl = builder.getContentByLine(5, 'left',
+            element.$.diffTable);
+        const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+        assert.isTrue(lineNumberEl.classList.contains('left'));
+      });
+
+      test('_getLineNumberEl side-by-side right', () => {
+        const contentEl = builder.getContentByLine(5, 'right',
+            element.$.diffTable);
+        const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+        assert.isTrue(lineNumberEl.classList.contains('right'));
+      });
+
+      test('_getLineNumberEl unified left', done => {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render(keyLocations, prefs).then(() => {
+          builder = element._builder;
+
+          const contentEl = builder.getContentByLine(5, 'left',
+              element.$.diffTable);
+          const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+          assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+          assert.isTrue(lineNumberEl.classList.contains('left'));
+          done();
+        });
+      });
+
+      test('_getLineNumberEl unified right', done => {
+        // Re-render as unified:
+        element.viewMode = 'UNIFIED_DIFF';
+        element.render(keyLocations, prefs).then(() => {
+          builder = element._builder;
+
+          const contentEl = builder.getContentByLine(5, 'right',
+              element.$.diffTable);
+          const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+          assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+          assert.isTrue(lineNumberEl.classList.contains('right'));
+          done();
+        });
+      });
+
       test('_getNextContentOnSide side-by-side left', () => {
         const startElem = builder.getContentByLine(5, 'left',
             element.$.diffTable);
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 860d900..485b0bb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -37,6 +37,7 @@
 
   Polymer({
     is: 'gr-diff-cursor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
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 6419a7c..ad9e99b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-diff-highlight',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** @type {!Array<!Gerrit.HoveredRange>} */
@@ -66,16 +67,21 @@
      * @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.
+     *     event, in which case we might want to immediately create a comment,
+     *     because isMouseUp === true combined with an existing selection must
+     *     mean that this is the end of a double-click.
      */
     handleSelectionChange(selection, isMouseUp) {
-      // 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();
+      // Debounce is not just nice for waiting until the selection has settled,
+      // it is also vital for being able to click on the action box before it is
+      // removed.
+      // If you wait longer than 50 ms, then you don't properly catch a very
+      // quick 'c' press after the selection change. If you wait less than 10
+      // ms, then you will have about 50 _handleSelection calls when doing a
+      // simple drag for select.
       this.debounce(
           'selectionChange', () => this._handleSelection(selection, isMouseUp),
-          200);
+          10);
     },
 
     _getThreadEl(e) {
@@ -297,50 +303,67 @@
       actionBox.placeBelow(range);
     },
 
-    _handleSelection(selection, isMouseUp) {
-      const normalizedRange = this._getNormalizedRange(selection);
-      if (!normalizedRange) {
-        return;
+    _isRangeValid(range) {
+      if (!range || !range.start || !range.end) {
+        return false;
       }
-      const domRange = selection.getRangeAt(0);
-      /** @type {?} */
-      const start = normalizedRange.start;
-      if (!start) {
-        return;
-      }
-      const end = normalizedRange.end;
-      if (!end) {
-        return;
-      }
+      const start = range.start;
+      const end = range.end;
       if (start.side !== end.side ||
           end.line < start.line ||
           (start.line === end.line && start.column === end.column)) {
+        return false;
+      }
+      return true;
+    },
+
+    _handleSelection(selection, isMouseUp) {
+      const normalizedRange = this._getNormalizedRange(selection);
+      if (!this._isRangeValid(normalizedRange)) {
+        this._removeActionBox();
         return;
       }
+      const domRange = selection.getRangeAt(0);
+      const start = normalizedRange.start;
+      const end = normalizedRange.end;
 
       // TODO (viktard): Drop empty first and last lines from selection.
 
+      // If the selection is from the end of one line to the start of the next
+      // line, then this must have been a double-click, or you have started
+      // dragging. Showing the action box is bad in the former case and not very
+      // useful in the latter, so never do that.
       // If this was a mouse-up event, we create a comment immediately if
       // the selection is from the end of a line to the start of the next line.
-      // 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) {
+      // In a perfect world we would only do this for double-click, but it is
+      // extremely rare that a user would drag from the end of one line to the
+      // start of the next and release the mouse, so we don't bother.
+      // TODO(brohlfs): This does not work, if the double-click is before a new
+      // diff chunk (start will be equal to end), and neither before an "expand
+      // the diff context" block (end line will match the first line of the new
+      // section and thus be greater than start line + 1).
+      if (start.line === end.line - 1 && end.column === 0) {
+        // Rather than trying to find the line contents (for comparing
+        // start.column with the content length), we just check if the selection
+        // is empty to see that it's at the end of a line.
         const content = domRange.cloneContents().querySelector('.contentText');
-        if (this._getLength(content) === 0) {
+        if (isMouseUp && this._getLength(content) === 0) {
           this.fire('create-range-comment', {side: start.side, range: {
             start_line: start.line,
             start_character: 0,
             end_line: start.line,
             end_character: start.column,
           }});
-          return;
         }
+        return;
       }
 
-      const actionBox = document.createElement('gr-selection-action-box');
-      const root = Polymer.dom(this.root);
-      root.insertBefore(actionBox, root.firstElementChild);
+      let actionBox = this.$$('gr-selection-action-box');
+      if (!actionBox) {
+        actionBox = document.createElement('gr-selection-action-box');
+        const root = Polymer.dom(this.root);
+        root.insertBefore(actionBox, root.firstElementChild);
+      }
       actionBox.range = {
         start_line: start.line,
         start_character: start.column,
@@ -368,10 +391,6 @@
       this._removeActionBox();
     },
 
-    _removeActionBoxDebounced() {
-      this.debounce('removeActionBox', this._removeActionBox, 10);
-    },
-
     _removeActionBox() {
       const actionBox = this.$$('gr-selection-action-box');
       if (actionBox) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index fd460cc..94f6ce9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -28,6 +28,13 @@
     UNIFIED: 'UNIFIED_DIFF',
   };
 
+  /** @enum {string} */
+  const TimingLabel = {
+    TOTAL: 'Diff Total Render',
+    CONTENT: 'Diff Content Render',
+    SYNTAX: 'Diff Syntax Render',
+  };
+
   const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
 
   /**
@@ -59,6 +66,7 @@
    */
   Polymer({
     is: 'gr-diff-host',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -201,6 +209,12 @@
       'comment-discard': '_handleCommentDiscard',
       'comment-update': '_handleCommentUpdate',
       'comment-save': '_handleCommentSave',
+
+      'render-start': '_handleRenderStart',
+      'render-content': '_handleRenderContent',
+      'render-syntax': '_handleRenderSyntax',
+
+      'normalize-range': '_handleNormalizeRange',
     },
 
     observers: [
@@ -327,7 +341,9 @@
      * @return {!Array<!HTMLElement>}
      */
     getThreadEls() {
-      return Polymer.dom(this.$.diff).querySelectorAll('.comment-thread');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.$.diff).querySelectorAll('.comment-thread'));
     },
 
     /** @param {HTMLElement} el */
@@ -825,5 +841,26 @@
       return this.comments[side].findIndex(
           item => item.__draftID === comment.__draftID);
     },
+
+    _handleRenderStart() {
+      this.$.reporting.time(TimingLabel.TOTAL);
+      this.$.reporting.time(TimingLabel.CONTENT);
+    },
+
+    _handleRenderContent() {
+      this.$.reporting.timeEnd(TimingLabel.CONTENT);
+      this.$.reporting.time(TimingLabel.SYNTAX);
+    },
+
+    _handleRenderSyntax() {
+      this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+      this.$.reporting.timeEnd(TimingLabel.TOTAL);
+    },
+
+    _handleNormalizeRange(event) {
+      this.$.reporting.reportInteraction('normalize-range',
+          `Modified invalid comment range on l. ${event.detail.lineNum}` +
+          ` of the ${event.detail.side} side`);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index ab9daec..7466ade 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
@@ -45,6 +45,11 @@
       stub('gr-rest-api-interface', {
         async getLoggedIn() { return getLoggedIn; },
       });
+      stub('gr-reporting', {
+        time: sandbox.stub(),
+        timeEnd: sandbox.stub(),
+      });
+
       element = fixture('basic');
     });
 
@@ -261,6 +266,38 @@
       assert.equal(attachedThreads[0].rootId, 42);
     });
 
+    suite('render reporting', () => {
+      test('starts total and content timer on render-start', done => {
+        element.dispatchEvent(
+            new CustomEvent('render-start', {bubbles: true}));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Total Render'));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Content Render'));
+        done();
+      });
+
+      test('ends content and starts syntax timer on render-content', done => {
+        element.dispatchEvent(
+            new CustomEvent('render-content', {bubbles: true}));
+        assert.isTrue(element.$.reporting.time.calledWithExactly(
+            'Diff Syntax Render'));
+        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+            'Diff Content Render'));
+        done();
+      });
+
+      test('ends total and syntax timer on render-syntax', done => {
+        element.dispatchEvent(
+            new CustomEvent('render-syntax', {bubbles: true}));
+        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+            'Diff Total Render'));
+        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+            'Diff Syntax Render'));
+        done();
+      });
+    });
+
     test('reload() cancels before network resolves', () => {
       const cancelStub = sandbox.stub(element.$.diff, 'cancel');
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index 88dd91a..e2d6a28 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-diff-mode-selector',
+    _legacyUndefinedCheck: true,
 
     properties: {
       mode: {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
new file mode 100644
index 0000000..ae53f76
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
@@ -0,0 +1,80 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+
+<dom-module id="gr-diff-preferences-dialog">
+  <template>
+    <style include="shared-styles">
+      .diffHeader,
+      .diffActions {
+        padding: 1em 1.5em;
+      }
+      .diffHeader,
+      .diffActions {
+        background-color: var(--dialog-background-color);
+      }
+      .diffHeader {
+        border-bottom: 1px solid var(--border-color);
+        font-weight: var(--font-weight-bold);
+      }
+      .diffActions {
+        border-top: 1px solid var(--border-color);
+        display: flex;
+        justify-content: flex-end;
+      }
+      .diffPrefsOverlay gr-button {
+        margin-left: 1em;
+      }
+      div.edited:after {
+        color: var(--deemphasized-text-color);
+        content: ' *';
+      }
+      #diffPreferences {
+        display: flex;
+        padding: .35em 1.5em;
+      }
+    </style>
+    <gr-overlay id="diffPrefsOverlay" with-backdrop>
+      <div class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
+      <gr-diff-preferences
+          id="diffPreferences"
+          diff-prefs="{{diffPrefs}}"
+          has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+      <div class="diffActions">
+        <gr-button
+            id="cancelButton"
+            link
+            on-tap="_handleCancelDiff">
+            Cancel
+        </gr-button>
+        <gr-button
+            id="saveButton"
+            link primary
+            on-tap="_handleSaveDiffPreferences"
+            disabled$="[[!_diffPrefsChanged]]">
+            Save
+        </gr-button>
+      </div>
+    </gr-overlay>
+  </template>
+  <script src="gr-diff-preferences-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
new file mode 100644
index 0000000..bcc3c2c
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-preferences-dialog',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      /** @type {?} */
+      diffPrefs: Object,
+
+      _diffPrefsChanged: Boolean,
+    },
+
+    getFocusStops() {
+      return {
+        start: this.$.contextSelect,
+        end: this.$.saveButton,
+      };
+    },
+
+    resetFocus() {
+      this.$.contextSelect.focus();
+    },
+
+    _computeHeaderClass(changed) {
+      return changed ? 'edited' : '';
+    },
+
+    _handleCancelDiff(e) {
+      e.stopPropagation();
+      this.$.diffPrefsOverlay.close();
+    },
+
+    open() {
+      this.$.diffPrefsOverlay.open().then(() => {
+        const focusStops = this.getFocusStops();
+        this.$.diffPrefsOverlay.setFocusStops(focusStops);
+        this.resetFocus();
+      });
+    },
+
+    _handleSaveDiffPreferences() {
+      this.$.diffPreferences.save().then(() => {
+        this.fire('reload-diff-preference', null, {bubbles: false});
+
+        this.$.diffPrefsOverlay.close();
+      });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
deleted file mode 100644
index a22f689..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ /dev/null
@@ -1,173 +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.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-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="../../../styles/shared-styles.html">
-
-<dom-module id="gr-diff-preferences">
-  <template>
-    <style include="shared-styles">
-      :host {
-        display: block;
-      }
-      :host([disabled]) {
-        opacity: .5;
-        pointer-events: none;
-      }
-      input,
-      select {
-        font: inherit;
-      }
-      input[type="number"] {
-        width: 4em;
-      }
-      .header,
-      .actions {
-        padding: 1em 1.5em;
-      }
-      .header,
-      .mainContainer,
-      .actions {
-        background-color: var(--dialog-background-color);
-      }
-      .header {
-        border-bottom: 1px solid var(--border-color);
-        font-weight: var(--font-weight-bold);
-      }
-      .mainContainer {
-        padding: 1em 0;
-      }
-      .pref {
-        align-items: center;
-        display: flex;
-        padding: .35em 1.5em;
-        width: 25em;
-      }
-      .pref:hover {
-        background-color: var(--hover-background-color);
-      }
-      .pref label {
-        cursor: pointer;
-        flex: 1;
-      }
-      .actions {
-        border-top: 1px solid var(--border-color);
-        display: flex;
-        justify-content: flex-end;
-      }
-      gr-button {
-        margin-left: 1em;
-      }
-    </style>
-    <gr-overlay id="prefsOverlay" with-backdrop>
-      <div class="header">
-        Diff View Preferences
-      </div>
-      <div class="mainContainer">
-        <div class="pref">
-          <label for="contextSelect">Context</label>
-          <select id="contextSelect" on-change="_handleContextSelectChange">
-            <option value="3">3 lines</option>
-            <option value="10">10 lines</option>
-            <option value="25">25 lines</option>
-            <option value="50">50 lines</option>
-            <option value="75">75 lines</option>
-            <option value="100">100 lines</option>
-            <option value="-1">Whole file</option>
-          </select>
-        </div>
-        <div class="pref">
-          <label for="lineWrappingInput">Fit to screen</label>
-          <input
-              is="iron-input"
-              type="checkbox"
-              id="lineWrappingInput"
-              on-tap="_handlelineWrappingTap">
-        </div>
-        <div class="pref" id="columnsPref">
-          <label for="columnsInput">Diff width</label>
-          <input is="iron-input" type="number" id="columnsInput"
-              prevent-invalid-input
-              allowed-pattern="[0-9]"
-              bind-value="{{_newPrefs.line_length}}">
-        </div>
-        <div class="pref">
-          <label for="tabSizeInput">Tab width</label>
-          <input is="iron-input" type="number" id="tabSizeInput"
-              prevent-invalid-input
-              allowed-pattern="[0-9]"
-              bind-value="{{_newPrefs.tab_size}}">
-        </div>
-        <div class="pref" hidden$="[[!_newPrefs.font_size]]">
-          <label for="fontSizeInput">Font size</label>
-          <input is="iron-input" type="number" id="fontSizeInput"
-                prevent-invalid-input
-                allowed-pattern="[0-9]"
-                bind-value="{{_newPrefs.font_size}}">
-        </div>
-        <div class="pref">
-          <label for="showTabsInput">Show tabs</label>
-          <input is="iron-input" type="checkbox" id="showTabsInput"
-              on-tap="_handleShowTabsTap">
-        </div>
-        <div class="pref">
-          <label for="showTrailingWhitespaceInput">
-            Show trailing whitespace</label>
-          <input is="iron-input" type="checkbox"
-              id="showTrailingWhitespaceInput"
-              on-tap="_handleShowTrailingWhitespaceTap">
-        </div>
-        <div class="pref">
-          <label for="syntaxHighlightInput">Syntax highlighting</label>
-          <input is="iron-input" type="checkbox" id="syntaxHighlightInput"
-              on-tap="_handleSyntaxHighlightTap">
-        </div>
-        <div class="pref">
-          <label for="automaticReviewInput">Automatically mark viewed files reviewed</label>
-          <input
-              is="iron-input"
-              id="automaticReviewInput"
-              type="checkbox"
-              on-tap="_handleAutomaticReviewTap">
-        </div>
-        <div class="pref">
-          <label for="ignoreWhitespace">Ignore Whitespace</label>
-          <select id="ignoreWhitespace" on-change="_handleIgnoreWhitespaceChange">
-            <option value="IGNORE_NONE">None</option>
-            <option value="IGNORE_TRAILING">Trailing</option>
-            <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
-            <option value="IGNORE_ALL">All</option>
-          </select>
-        </div>
-      </div>
-      <div class="actions">
-        <gr-button id="cancelButton" link on-tap="_handleCancel">
-            Cancel</gr-button>
-        <gr-button id="saveButton" link primary on-tap="_handleSave">
-            Save</gr-button>
-      </div>
-    </gr-overlay>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-    <gr-storage id="storage"></gr-storage>
-  </template>
-  <script src="gr-diff-preferences.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
deleted file mode 100644
index 8fc90b9..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-diff-preferences',
-
-    properties: {
-      prefs: {
-        type: Object,
-        notify: true,
-      },
-      localPrefs: {
-        type: Object,
-        notify: true,
-      },
-      disabled: {
-        type: Boolean,
-        value: false,
-        reflectToAttribute: true,
-      },
-
-      /** @type {?} */
-      _newPrefs: Object,
-      _newLocalPrefs: Object,
-    },
-
-    observers: [
-      '_prefsChanged(prefs.*)',
-      '_localPrefsChanged(localPrefs.*)',
-    ],
-
-    getFocusStops() {
-      return {
-        start: this.$.contextSelect,
-        end: this.$.saveButton,
-      };
-    },
-
-    resetFocus() {
-      this.$.contextSelect.focus();
-    },
-
-    _prefsChanged(changeRecord) {
-      const prefs = changeRecord.base;
-      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
-      // an object as a value, it must be marked enumerable.
-      this._newPrefs = Object.assign({}, prefs);
-      this.$.contextSelect.value = prefs.context;
-      this.$.showTabsInput.checked = prefs.show_tabs;
-      this.$.showTrailingWhitespaceInput.checked = prefs.show_whitespace_errors;
-      this.$.lineWrappingInput.checked = prefs.line_wrapping;
-      this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting;
-      this.$.automaticReviewInput.checked = !prefs.manual_review;
-      this.$.ignoreWhitespace.value = prefs.ignore_whitespace;
-    },
-
-    _localPrefsChanged(changeRecord) {
-      const localPrefs = changeRecord.base || {};
-      this._newLocalPrefs = Object.assign({}, localPrefs);
-    },
-
-    _handleContextSelectChange(e) {
-      const selectEl = Polymer.dom(e).rootTarget;
-      this.set('_newPrefs.context', parseInt(selectEl.value, 10));
-    },
-
-    _handleIgnoreWhitespaceChange(e) {
-      const selectEl = Polymer.dom(e).rootTarget;
-      this.set('_newPrefs.ignore_whitespace', selectEl.value);
-    },
-
-    _handleShowTabsTap(e) {
-      this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleShowTrailingWhitespaceTap(e) {
-      this.set('_newPrefs.show_whitespace_errors',
-          Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleSyntaxHighlightTap(e) {
-      this.set('_newPrefs.syntax_highlighting',
-          Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handlelineWrappingTap(e) {
-      this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleAutomaticReviewTap(e) {
-      this.set('_newPrefs.manual_review', !Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleSave(e) {
-      e.stopPropagation();
-      this.prefs = this._newPrefs;
-      this.localPrefs = this._newLocalPrefs;
-      const el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this.$.storage.savePreferences(this._localPrefs);
-      this._saveDiffPreferences().then(response => {
-        el.disabled = false;
-        if (!response.ok) { return response; }
-
-        this.$.prefsOverlay.close();
-      }).catch(err => {
-        el.disabled = false;
-      });
-    },
-
-    _handleCancel(e) {
-      e.stopPropagation();
-      this.$.prefsOverlay.close();
-    },
-
-    open() {
-      this.$.prefsOverlay.open().then(() => {
-        const focusStops = this.getFocusStops();
-        this.$.prefsOverlay.setFocusStops(focusStops);
-        this.resetFocus();
-      });
-    },
-
-    _saveDiffPreferences() {
-      return this.$.restAPI.saveDiffPreferences(this.prefs);
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
deleted file mode 100644
index d9e14c0..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ /dev/null
@@ -1,110 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-preferences</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-preferences.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-preferences></gr-diff-preferences>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-preferences tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('model changes', () => {
-      element.prefs = {
-        context: 10,
-        font_size: 12,
-        line_length: 100,
-        show_tabs: true,
-        tab_size: 8,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-      };
-      assert.deepEqual(element.prefs, element._newPrefs);
-
-      element.$.contextSelect.value = '50';
-      element.fire('change', {}, {node: element.$.contextSelect});
-      element.$.columnsInput.bindValue = 80;
-      element.$.fontSizeInput.bindValue = 10;
-      element.$.tabSizeInput.bindValue = 4;
-      MockInteractions.tap(element.$.showTabsInput);
-      MockInteractions.tap(element.$.showTrailingWhitespaceInput);
-      MockInteractions.tap(element.$.syntaxHighlightInput);
-      MockInteractions.tap(element.$.lineWrappingInput);
-
-      assert.equal(element._newPrefs.context, 50);
-      assert.equal(element._newPrefs.font_size, 10);
-      assert.equal(element._newPrefs.line_length, 80);
-      assert.equal(element._newPrefs.tab_size, 4);
-      assert.isFalse(element._newPrefs.show_tabs);
-      assert.isFalse(element._newPrefs.show_whitespace_errors);
-      assert.isTrue(element._newPrefs.line_wrapping);
-      assert.isFalse(element._newPrefs.syntax_highlighting);
-    });
-
-    test('clicking save button calls _handleSave function', () => {
-      const savePrefs = sinon.stub(element, '_handleSave');
-      MockInteractions.tap(element.$.saveButton);
-      flushAsynchronousOperations();
-      assert(savePrefs.calledOnce);
-      savePrefs.restore();
-    });
-
-    test('save button', () => {
-      element.prefs = {
-        font_size: '11',
-      };
-      element._newPrefs = {
-        font_size: '12',
-      };
-      const saveStub = sandbox.stub(element.$.restAPI, 'saveDiffPreferences',
-          () => { return Promise.resolve(); });
-
-      MockInteractions.tap(element.$$('gr-button[primary]'));
-      assert.deepEqual(element.prefs, element._newPrefs);
-      assert.deepEqual(saveStub.lastCall.args[0], element._newPrefs);
-    });
-
-    test('cancel button', () => {
-      const closeStub = sandbox.stub(element.$.prefsOverlay, 'close');
-      MockInteractions.tap(element.$$('gr-button:not([primary])'));
-      assert.isTrue(closeStub.called);
-    });
-  });
-</script>
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..a1af8e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -45,8 +45,22 @@
    */
   const MAX_GROUP_SIZE = 120;
 
+  /**
+   * Converts the API's `DiffContent`s  to `GrDiffGroup`s for rendering.
+   *
+   * This includes a number of tasks:
+   *  - adding a group for the "File" pseudo line that file-level comments can
+   *    be attached to
+   *  - replacing unchanged parts of the diff that are outside the user's
+   *    context setting and do not have comments with a group representing the
+   *    "expand context" widget. This may require splitting a `DiffContent` so
+   *    that the part that is within the context or has comments is shown, while
+   *    the rest is not.
+   *  - splitting large `DiffContent`s to allow more granular async rendering
+   */
   Polymer({
     is: 'gr-diff-processor',
+    _legacyUndefinedCheck: true,
 
     properties: {
 
@@ -80,8 +94,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 +132,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 +143,65 @@
       // so finish processing.
       if (isBinary) { return Promise.resolve(); }
 
-      return new Promise(resolve => {
-        const state = {
-          lineNums: {left: 0, right: 0},
-          sectionIndex: 0,
-        };
 
-        content = this._splitCommonGroupsWithComments(content);
+      this._processPromise = util.makeCancelable(
+          new Promise(resolve => {
+            const state = {
+              lineNums: {left: 0, right: 0},
+              sectionIndex: 0,
+            };
 
-        let currentBatch = 0;
-        const nextStep = () => {
-          if (this._isScrolling) {
-            this.async(nextStep, 100);
-            return;
-          }
-          // If we are done, resolve the promise.
-          if (state.sectionIndex >= content.length) {
-            resolve(this.groups);
-            this._nextStepHandle = undefined;
-            return;
-          }
+            content = this._splitLargeChunks(content);
+            content = this._splitUnchangedChunksWithComments(content);
 
-          // Process the next section and incorporate the result.
-          const result = this._processNext(state, content);
-          for (const group of result.groups) {
-            this.push('groups', group);
-            currentBatch += group.lines.length;
-          }
-          state.lineNums.left += result.lineDelta.left;
-          state.lineNums.right += result.lineDelta.right;
+            let currentBatch = 0;
+            const nextStep = () => {
+              if (this._isScrolling) {
+                this.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();
       }
     },
 
@@ -349,60 +385,78 @@
       return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
     },
 
+
+    /**
+     * Split chunks into smaller chunks of the same kind.
+     *
+     * This is done to prevent doing too much work on the main thread in one
+     * uninterrupted rendering step, which would make the browser unresponsive.
+     *
+     * Note that in the case of unmodified chunks, we only split chunks if the
+     * context is set to file (because otherwise they are split up further down
+     * the processing into the visible and hidden context), and only split it
+     * into 2 chunks, one max sized one and the rest (for reasons that are
+     * unclear to me).
+     *
+     * @param {!Array<!Object>} chunks Chunks as returned from the server
+     * @return {!Array<!Object>} Finer grained chunks.
+     */
+    _splitLargeChunks(chunks) {
+      const newChunks = [];
+
+      for (const chunk of chunks) {
+        if (!chunk.ab) {
+          for (const group of this._breakdownGroup(chunk)) {
+            newChunks.push(group);
+          }
+          continue;
+        }
+
+        // If the context is set to "whole file", then break down the shared
+        // chunks so they can be rendered incrementally. Note: this is not
+        // enabled for any other context preference because manipulating the
+        // chunks in this way violates assumptions by the context grouper logic.
+        if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
+          // Split large shared groups in two, where the first is the maximum
+          // group size.
+          newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
+          newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
+        } else {
+          newChunks.push(chunk);
+        }
+      }
+      return newChunks;
+    },
+
     /**
      * In order to show comments out of the bounds of the selected context,
      * treat them as separate chunks within the model so that the content (and
      * context surrounding it) renders correctly.
-     * @param {?} content The diff content object. (has to be iterable)
-     * @return {!Object} A new diff content object with regions split up.
+     * @param {!Array<!Object>} chunks DiffContents as returned from server.
+     * @return {!Array<!Object>} Finer grained DiffContents.
      */
-    _splitCommonGroupsWithComments(content) {
+    _splitUnchangedChunksWithComments(chunks) {
       const result = [];
       let leftLineNum = 0;
       let rightLineNum = 0;
 
-      // If the context is set to "whole file", then break down the shared
-      // chunks so they can be rendered incrementally. Note: this is not enabled
-      // for any other context preference because manipulating the chunks in
-      // this way violates assumptions by the context grouper logic.
-      if (this.context === -1) {
-        const newContent = [];
-        for (const group of content) {
-          if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) {
-            // Split large shared groups in two, where the first is the maximum
-            // group size.
-            newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)});
-            newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)});
-          } else {
-            newContent.push(group);
+      for (const chunk of chunks) {
+        // If it isn't a common chunk, append it as-is and update line numbers.
+        if (!chunk.ab) {
+          if (chunk.a) {
+            leftLineNum += chunk.a.length;
           }
-        }
-        content = newContent;
-      }
-
-      // For each section in the diff.
-      for (let i = 0; i < content.length; i++) {
-        // If it isn't a common group, append it as-is and update line numbers.
-        if (!content[i].ab) {
-          if (content[i].a) {
-            leftLineNum += content[i].a.length;
+          if (chunk.b) {
+            rightLineNum += chunk.b.length;
           }
-          if (content[i].b) {
-            rightLineNum += content[i].b.length;
-          }
-
-          for (const group of this._breakdownGroup(content[i])) {
-            result.push(group);
-          }
-
+          result.push(chunk);
           continue;
         }
 
-        const chunk = content[i].ab;
         let currentChunk = {ab: []};
 
         // For each line in the common group.
-        for (const subChunk of chunk) {
+        for (const line of chunk.ab) {
           leftLineNum++;
           rightLineNum++;
 
@@ -418,10 +472,10 @@
             }
 
             // Add the non-collapse line as its own chunk.
-            result.push({ab: [subChunk]});
+            result.push({ab: [line]});
           } else {
             // Append the current line to the current chunk.
-            currentChunk.ab.push(subChunk);
+            currentChunk.ab.push(line);
           }
         }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 7ccd9f8..186a49e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -280,7 +280,6 @@
           left: {1: true},
           right: {10: true},
         };
-        const lineNums = {left: 0, right: 0};
 
         const content = [
           {
@@ -304,7 +303,7 @@
           },
         ];
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, [
           {
             ab: ['Copyright (C) 2015 The Android Open Source Project'],
@@ -337,28 +336,25 @@
         ]);
       });
 
-      test('breaks-down shared chunks w/ whole-file', () => {
+      test('breaks down shared chunks w/ whole-file', () => {
         const size = 120 * 2 + 5;
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(size, () => { return `${Math.random()}`; }),
         }];
         element.context = -1;
-        const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+        const result = element._splitLargeChunks(content);
         assert.equal(result.length, 2);
         assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
         assert.deepEqual(result[1].ab, content[0].ab.slice(120));
       });
 
       test('does not break-down shared chunks w/ context', () => {
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(75, () => { return `${Math.random()}`; }),
         }];
         element.context = 4;
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, content);
       });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 6a9d88f..072a0d7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -32,6 +32,7 @@
 
   Polymer({
     is: 'gr-diff-selection',
+    _legacyUndefinedCheck: true,
 
     properties: {
       diff: Object,
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 b3210cc..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
@@ -34,9 +34,9 @@
 <link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../gr-diff-host/gr-diff-host.html">
+<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
+<link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
 <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff-view">
@@ -338,10 +338,11 @@
         on-comment-anchor-tap="_onLineSelected"
         on-line-selected="_onLineSelected">
     </gr-diff-host>
-    <gr-diff-preferences
-        id="diffPreferences"
-        prefs="{{_prefs}}"
-        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
+    <gr-diff-preferences-dialog
+        id="diffPreferencesDialog"
+        diff-prefs="{{_prefs}}"
+        on-reload-diff-preference="_handleReloadingDiffPreference">
+    </gr-diff-preferences-dialog>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="cursor"></gr-diff-cursor>
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 dadf8a7..d37c97d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -35,6 +35,7 @@
 
   Polymer({
     is: 'gr-diff-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -266,7 +267,9 @@
     },
 
     _getDiffPreferences() {
-      return this.$.restAPI.getDiffPreferences();
+      return this.$.restAPI.getDiffPreferences().then(prefs => {
+        this._prefs = prefs;
+      });
     },
 
     _getPreferences() {
@@ -466,7 +469,7 @@
       if (this._diffPrefsDisabled) { return; }
 
       e.preventDefault();
-      this.$.diffPreferences.open();
+      this.$.diffPreferencesDialog.open();
     },
 
     _handleToggleDiffMode(e) {
@@ -617,10 +620,7 @@
 
       const promises = [];
 
-      this._localPrefs = this.$.storage.getPreferences();
-      promises.push(this._getDiffPreferences().then(prefs => {
-        this._prefs = prefs;
-      }));
+      promises.push(this._getDiffPreferences());
 
       promises.push(this._getPreferences().then(prefs => {
         this._userPrefs = prefs;
@@ -846,22 +846,7 @@
 
     _handlePrefsTap(e) {
       e.preventDefault();
-      this.$.diffPreferences.open();
-    },
-
-    _handlePrefsSave(e) {
-      e.stopPropagation();
-      const el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this.$.storage.savePreferences(this._localPrefs);
-      this._saveDiffPreferences().then(response => {
-        el.disabled = false;
-        if (!response.ok) { return response; }
-
-        this.$.prefsOverlay.close();
-      }).catch(err => {
-        el.disabled = false;
-      });
+      this.$.diffPreferencesDialog.open();
     },
 
     /**
@@ -1049,5 +1034,9 @@
           (file === this._path || !this._reviewedFiles.has(file)));
       this._navToFile(this._path, unreviewedFiles, 1);
     },
+
+    _handleReloadingDiffPreference() {
+      this._getDiffPreferences();
+    },
   });
 })();
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 0274330..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
@@ -173,7 +173,7 @@
       assert.isTrue(element._loading);
 
       const showPrefsStub =
-          sandbox.stub(element.$.diffPreferences.$.prefsOverlay, 'open',
+          sandbox.stub(element.$.diffPreferencesDialog, 'open',
               () => Promise.resolve());
 
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
@@ -386,7 +386,7 @@
 
     test('prefsButton opens gr-diff-preferences', () => {
       const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
-      const overlayOpenStub = sandbox.stub(element.$.diffPreferences,
+      const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
           'open');
       const prefsButton =
           Polymer.dom(element.root).querySelector('.prefsButton');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index 88fcd0e..dd69724 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -20,11 +20,20 @@
   // Prevent redefinition.
   if (window.GrDiffGroup) { return; }
 
+  /**
+   * A chunk of the diff that should be rendered together.
+   */
   function GrDiffGroup(type, opt_lines) {
     this.type = type;
+
+    /** @type{!Array<!GrDiffLine>} */
     this.lines = [];
+    /** @type{!Array<!GrDiffLine>} */
     this.adds = [];
+    /** @type{!Array<!GrDiffLine>} */
     this.removes = [];
+
+    /** @type{boolean|undefined} */
     this.dueToRebase = undefined;
 
     this.lineRange = {
@@ -40,8 +49,13 @@
   GrDiffGroup.prototype.element = null;
 
   GrDiffGroup.Type = {
+    /** Unchanged context. */
     BOTH: 'both',
+
+    /** A widget used to show more context. */
     CONTEXT_CONTROL: 'contextControl',
+
+    /** Added, removed or modified chunk. */
     DELTA: 'delta',
   };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 4ccdf96..61e8603 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -184,7 +184,12 @@
         /* >> character */
         content: '\00BB';
       }
-      .trailing-whitespace {
+      /* Is defined after other background-colors, such that this
+         rule wins in case of same specificity. */
+      .trailing-whitespace,
+      .content .trailing-whitespace,
+      .trailing-whitespace .intraline,
+      .content .trailing-whitespace .intraline {
         border-radius: .4em;
         background-color: var(--diff-trailing-whitespace-indicator);
       }
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 9fc9232..24f167d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -92,8 +92,21 @@
     return content;
   };
 
+  const COMMIT_MSG_PATH = '/COMMIT_MSG';
+  /**
+   * 72 is the inofficial length standard for git commit messages.
+   * Derived from the fact that git log/show appends 4 ws in the beginning of
+   * each line when displaying commit messages. To center the commit message
+   * in an 80 char terminal a 4 ws border is added to the rightmost side:
+   * 4 + 72 + 4
+   */
+  const COMMIT_MSG_LINE_LENGTH = 72;
+
+  const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+
   Polymer({
     is: 'gr-diff',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the user selects a line.
@@ -106,11 +119,18 @@
      * @event show-auth-required
      */
 
-     /**
-      * Fired when a comment is created
-      *
-      * @event create-comment
-      */
+    /**
+     * Fired when a comment is created
+     *
+     * @event create-comment
+     */
+
+    /**
+     * 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,
@@ -120,7 +140,10 @@
       },
       /** @type {?} */
       patchRange: Object,
-      path: String,
+      path: {
+        type: String,
+        observer: '_pathObserver',
+      },
       prefs: {
         type: Object,
         observer: '_prefsObserver',
@@ -158,21 +181,6 @@
        /** @type ?Defs.LineOfInterest */
       lineOfInterest: Object,
 
-      /**
-       * The key locations based on the comments and line of interests,
-       * where lines should not be collapsed.
-       *
-       * @type {{left: Object<(string|number), number>,
-       *     right: Object<(string|number), number>}}
-       */
-      _keyLocations: {
-        type: Object,
-        value: () => ({
-          left: {},
-          right: {},
-        }),
-      },
-
       loading: {
         type: Boolean,
         value: false,
@@ -321,7 +329,6 @@
         const addedThreadEls = info.addedNodes.filter(isThreadEl);
         const removedThreadEls = info.removedNodes.filter(isThreadEl);
         this._updateRanges(addedThreadEls, removedThreadEls);
-        this._updateKeyLocations(addedThreadEls, removedThreadEls);
         this._redispatchHoverEvents(addedThreadEls);
       });
     },
@@ -349,17 +356,29 @@
       this.push('_commentRanges', ...addedCommentRanges);
     },
 
-    _updateKeyLocations(addedThreadEls, removedThreadEls) {
-      for (const threadEl of addedThreadEls) {
-        const commentSide = threadEl.getAttribute('comment-side');
-        const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
-        this._keyLocations[commentSide][lineNum] = true;
+    /**
+     * 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;
       }
-      for (const threadEl of removedThreadEls) {
+      const threadEls = Polymer.dom(this).getEffectiveChildNodes()
+          .filter(isThreadEl);
+
+      for (const threadEl of threadEls) {
         const commentSide = threadEl.getAttribute('comment-side');
-        const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
-        this._keyLocations[commentSide][lineNum] = false;
+        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.
@@ -379,6 +398,7 @@
     /** Cancel any remaining diff builder rendering work. */
     cancel() {
       this.$.diffBuilder.cancel();
+      this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
     },
 
     /** @return {!Array<!HTMLElement>} */
@@ -387,7 +407,9 @@
         return [];
       }
 
-      return Polymer.dom(this.root).querySelectorAll('.diff-row');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this.root).querySelectorAll('.diff-row'));
     },
 
     /** @return {boolean} */
@@ -620,6 +642,11 @@
       }
     },
 
+    _pathObserver() {
+      // Call _prefsChanged(), because line-limit style value depends on path.
+      this._prefsChanged(this.prefs);
+    },
+
     _viewModeObserver() {
       this._prefsChanged(this.prefs);
     },
@@ -644,17 +671,19 @@
 
       this._blame = null;
 
+      const lineLength = this.path === COMMIT_MSG_PATH ?
+        COMMIT_MSG_LINE_LENGTH : prefs.line_length;
       const stylesToUpdate = {};
 
       if (prefs.line_wrapping) {
         this._diffTableClass = 'full-width';
         if (this.viewMode === 'SIDE_BY_SIDE') {
           stylesToUpdate['--content-width'] = 'none';
-          stylesToUpdate['--line-limit'] = prefs.line_length + 'ch';
+          stylesToUpdate['--line-limit'] = lineLength + 'ch';
         }
       } else {
         this._diffTableClass = '';
-        stylesToUpdate['--content-width'] = prefs.line_length + 'ch';
+        stylesToUpdate['--content-width'] = lineLength + 'ch';
       }
 
       if (prefs.font_size) {
@@ -664,17 +693,32 @@
       this.updateStyles(stylesToUpdate);
 
       if (this.diff && !this.noRenderOnPrefsChange) {
-        this._renderDiffTable();
+        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) {
@@ -691,11 +735,12 @@
 
       this._showWarning = false;
 
-      if (this.lineOfInterest) {
-        const side = this.lineOfInterest.leftSide ? 'left' : 'right';
-        this._keyLocations[side][this.lineOfInterest.number] = true;
-      }
-      this.$.diffBuilder.render(this._keyLocations, this._getBypassPrefs());
+      const keyLocations = this._computeKeyLocations();
+      this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
+          .then(() => {
+            this.dispatchEvent(
+                new CustomEvent('render', {bubbles: true}));
+          });
     },
 
     _handleRenderContent() {
@@ -775,12 +820,12 @@
 
     _handleFullBypass() {
       this._safetyBypass = FULL_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     _handleLimitedBypass() {
       this._safetyBypass = LIMITED_CONTEXT;
-      this._renderDiffTable();
+      this._debounceRenderDiffTable();
     },
 
     /** @return {string} */
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 300807c..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();
     });
@@ -81,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']);
     });
@@ -225,7 +227,7 @@
 
         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));
@@ -448,7 +450,7 @@
             binary: true,
           };
 
-          element.addEventListener('render', () => {
+          function rendered() {
             // Recognizes that it should be an image diff.
             assert.isTrue(element.isImageDiff);
             assert.instanceOf(
@@ -460,7 +462,9 @@
             assert.isNotOk(leftImage);
             assert.isOk(rightImage);
             done();
-          });
+            element.removeEventListener('render', rendered);
+          }
+          element.addEventListener('render', rendered);
 
           element.revisionImage = mockFile2;
           element.diff = mockDiff;
@@ -483,7 +487,7 @@
             binary: true,
           };
 
-          element.addEventListener('render', () => {
+          function rendered() {
             // Recognizes that it should be an image diff.
             assert.isTrue(element.isImageDiff);
             assert.instanceOf(
@@ -495,7 +499,9 @@
             assert.isOk(leftImage);
             assert.isNotOk(rightImage);
             done();
-          });
+            element.removeEventListener('render', rendered);
+          }
+          element.addEventListener('render', rendered);
 
           element.baseImage = mockFile1;
           element.diff = mockDiff;
@@ -519,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(
@@ -527,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;
@@ -679,12 +687,14 @@
             change_type: 'MODIFIED',
             content: [{skip: 66}],
           };
+          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);
         });
 
@@ -692,8 +702,9 @@
             '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);
         });
       });
@@ -760,33 +771,39 @@
       });
 
       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();
       });
     });
@@ -943,7 +960,8 @@
       setup(() => {
         element = fixture('basic');
         element.prefs = {};
-        renderStub = sandbox.stub(element.$.diffBuilder, 'render');
+        renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+            .returns(new Promise(() => {}));
       });
 
       test('lineOfInterest is a key location', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index cf8118f..ea8aa47 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -31,6 +31,7 @@
 
   Polymer({
     is: 'gr-patch-range-select',
+    _legacyUndefinedCheck: true,
 
     properties: {
       availablePatches: Array,
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
index 71b5bc3..c9e9f50 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
@@ -16,10 +16,8 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <dom-module id="gr-ranged-comment-layer">
   <template>
-    <gr-reporting id="reporting" category="comments"></gr-reporting>
   </template>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-ranged-comment-layer.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index 8cee1f4..e28142b 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
@@ -22,13 +22,21 @@
   const RANGE_HIGHLIGHT = 'range';
   const HOVER_HIGHLIGHT = 'rangeHighlight';
 
-  const NORMALIZE_RANGE_EVENT = 'normalize-range';
-
   /** @typedef {{side: string, range: Gerrit.Range, hovering: boolean}} */
   Gerrit.HoveredRange;
 
   Polymer({
     is: 'gr-ranged-comment-layer',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when the range in a range comment was malformed and had to be
+     * normalized.
+     *
+     * It's `detail` has a `lineNum` and `side` parameter.
+     *
+     * @event normalize-range
+     */
 
     properties: {
       /** @type {!Array<!Gerrit.HoveredRange>} */
@@ -51,9 +59,10 @@
      * 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 &&
@@ -177,9 +186,10 @@
             // @see Issue 5744
             if (range.start >= range.end && range.start < line.text.length) {
               range.end = line.text.length;
-              this.$.reporting.reportInteraction(NORMALIZE_RANGE_EVENT,
-                  'Modified invalid comment range on l.' + lineNum +
-                  ' of the ' + side + ' side');
+              this.dispatchEvent(new CustomEvent('normalize-range', {
+                bubbles: true,
+                detail: {lineNum, side},
+              }));
             }
 
             return range;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index c198ace..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
@@ -93,6 +93,7 @@
       let el;
       let line;
       let annotateElementStub;
+      const lineNumberEl = document.createElement('td');
 
       setup(() => {
         sandbox = sinon.sandbox.create();
@@ -111,7 +112,7 @@
         line.type = GrDiffLine.Type.REMOVE;
         line.beforeNumber = 40;
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -122,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;
@@ -140,7 +141,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;
@@ -157,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;
@@ -172,7 +173,7 @@
         line.beforeNumber = 36;
         el.setAttribute('data-side', 'right');
 
-        element.annotate(el, line);
+        element.annotate(el, lineNumberEl, line);
 
         assert.isFalse(annotateElementStub.called);
       });
@@ -185,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;
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 abf8e73..26bf738 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-selection-action-box',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the comment creation action was taken (hotkey, click).
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 f8d9e11..32724bd 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
@@ -22,6 +22,7 @@
     'application/json': 'json',
     'application/x-powershell': 'powershell',
     'application/typescript': 'typescript',
+    'application/xml': 'xml',
     'application/xquery': 'xquery',
     'application/x-erb': 'erb',
     'text/css': 'css',
@@ -65,6 +66,7 @@
     'text/x-perl': 'perl',
     'text/x-pgsql': 'pgsql', // postgresql
     'text/x-php': 'php',
+    'text/x-properties': 'properties',
     'text/x-protobuf': 'protobuf',
     'text/x-puppet': 'puppet',
     'text/x-python': 'python',
@@ -79,9 +81,12 @@
     'text/x-sh': 'bash',
     'text/x-sql': 'sql',
     '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',
     'text/x-yaml': 'yaml',
     'text/vbscript': 'vbscript',
   };
@@ -92,6 +97,7 @@
     'gr-diff gr-syntax gr-syntax-attribute': true,
     'gr-diff gr-syntax gr-syntax-built_in': true,
     'gr-diff gr-syntax gr-syntax-comment': true,
+    'gr-diff gr-syntax gr-syntax-function': true,
     'gr-diff gr-syntax gr-syntax-keyword': true,
     'gr-diff gr-syntax gr-syntax-link': true,
     'gr-diff gr-syntax gr-syntax-literal': true,
@@ -99,6 +105,7 @@
     'gr-diff gr-syntax gr-syntax-meta-keyword': true,
     'gr-diff gr-syntax gr-syntax-name': true,
     'gr-diff gr-syntax gr-syntax-number': true,
+    'gr-diff gr-syntax gr-syntax-params': true,
     'gr-diff gr-syntax gr-syntax-regexp': true,
     'gr-diff gr-syntax gr-syntax-selector-attr': true,
     'gr-diff gr-syntax gr-syntax-selector-class': true,
@@ -122,6 +129,7 @@
 
   Polymer({
     is: 'gr-syntax-layer',
+    _legacyUndefinedCheck: true,
 
     properties: {
       diff: {
@@ -148,6 +156,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,
     },
 
@@ -159,9 +177,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.
@@ -191,12 +210,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 = [];
@@ -205,13 +235,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();
@@ -226,49 +254,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() {
@@ -342,7 +376,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);
@@ -350,7 +385,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/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 41d3804..a122113 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -29,6 +29,12 @@
       .contentText {
         color: var(--syntax-default-color);
       }
+      .gr-syntax-attribute {
+        color: var(--syntax-attribute-color);
+      }
+      .gr-syntax-function {
+        color: var(--syntax-function-color);
+      }
       .gr-syntax-meta {
         color: var(--syntax-meta-color);
       }
@@ -94,6 +100,9 @@
       .gr-syntax-template-tag {
         color: var(--syntax-template-tag-color);
       }
+      .gr-syntax-param {
+        color: var(--syntax-param-color);
+      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index f850b9d..995326f 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-documentation-search',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 168ded8..01cd9df 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-default-editor',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the content of the editor changes.
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index 8740b0d..2658bd2 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-controls',
+    _legacyUndefinedCheck: true,
     properties: {
       change: Object,
       patchNum: String,
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 82010f5..9407f18 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-file-controls',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when an action in the overflow menu is tapped.
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index bf7fc99..8e108bb 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -26,6 +26,7 @@
 
   Polymer({
     is: 'gr-editor-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 533136c..e537ac4 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,16 +15,18 @@
 limitations under the License.
 -->
 <script>
-  // This must be set prior to loading Polymer for the first time.
-  if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
-    window.Polymer = {
-      dom: 'shadow',
-      passiveTouchGestures: true,
-    };
-  } else if (!window.Polymer) {
-    window.Polymer = {
-      passiveTouchGestures: true,
-    };
+  if (!window.POLYMER2) {
+    // This must be set prior to loading Polymer for the first time.
+    if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
+      window.Polymer = {
+        dom: 'shadow',
+        passiveTouchGestures: true,
+      };
+    } else if (!window.Polymer) {
+      window.Polymer = {
+        passiveTouchGestures: true,
+      };
+    }
   }
   // Needed for JSCompiler to understand it's global.
   // eslint-disable-next-line no-unused-vars, prefer-const
@@ -34,6 +36,7 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html">
+<link rel="import" href="../bower_components/polymer/lib/legacy/legacy-data-mixin.html">
 <link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
@@ -42,6 +45,7 @@
     safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
   });
 </script>
+<script src="../bower_components/moment/moment.js"></script>
 
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
@@ -146,6 +150,7 @@
         color: var(--error-text-color);
       }
     </style>
+    <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
     <gr-fixed-panel id="header">
       <gr-main-header
           id="mainHeader"
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index f2f06d1..a2a495b 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -20,12 +20,14 @@
   // Eagerly render Polymer components when backgrounded. (Skips
   // requestAnimationFrame.)
   // @see https://github.com/Polymer/polymer/issues/3851
-  // TODO: Reassess after Polymer 2.0 upgrade.
   // @see Issue 4699
-  Polymer.RenderStatus._makeReady();
+  if (!window.POLYMER2) {
+    Polymer.RenderStatus._makeReady();
+  }
 
   Polymer({
     is: 'gr-app',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the URL location changes.
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index cd59f7d..86238cf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -30,6 +30,7 @@
   <script>
     Polymer({
       is: 'some-element',
+      _legacyUndefinedCheck: true,
       properties: {
         fooBar: {
           type: Object,
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 230be0e..e0fa61e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -54,6 +54,7 @@
   GrDomHook.prototype._createPlaceholder = function(hookName) {
     Polymer({
       is: hookName,
+      _legacyUndefinedCheck: true,
       properties: {
         plugin: Object,
         content: Object,
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 5006461..35e4292 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-endpoint-decorator',
+    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
@@ -29,6 +30,16 @@
         type: Map,
         value() { return new Map(); },
       },
+      /**
+       * This map prevents importing the same endpoint twice.
+       * Without caching, if a plugin is loaded after the loaded plugins
+       * callback fires, it will be imported twice and appear twice on the page.
+       * @type {!Map}
+       */
+      _initializedPlugins: {
+        type: Map,
+        value() { return new Map(); },
+      },
     },
 
     detached() {
@@ -61,7 +72,9 @@
     },
 
     _getEndpointParams() {
-      return Polymer.dom(this).querySelectorAll('gr-endpoint-param');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(
+          Polymer.dom(this).querySelectorAll('gr-endpoint-param'));
     },
 
     /**
@@ -102,6 +115,9 @@
     },
 
     _initModule({moduleName, plugin, type, domHook}) {
+      if (this._initializedPlugins.get(plugin.getPluginName())) {
+        return;
+      }
       let initPromise;
       switch (type) {
         case 'decorate':
@@ -115,6 +131,7 @@
         console.warn('Unable to initialize module' +
             `${moduleName} from ${plugin.getPluginName()}`);
       }
+      this._initializedPlugins.set(plugin.getPluginName(), true);
       initPromise.then(el => {
         domHook.handleInstanceAttached(el);
         this._domHooks.set(el, domHook);
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index cbc3d6a..e21fc72 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-endpoint-param',
+    _legacyUndefinedCheck: true,
     properties: {
       name: String,
       value: {
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
index 709c042..47274f6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -30,6 +30,7 @@
   <script>
     Polymer({
       is: 'some-element',
+      _legacyUndefinedCheck: true,
       properties: {
         fooBar: {
           type: Object,
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 0e8bb45..7924e27 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-external-style',
+    _legacyUndefinedCheck: true,
 
     properties: {
       name: String,
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 111cdc6..4b5d4f0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-plugin-host',
+    _legacyUndefinedCheck: true,
 
     properties: {
       config: {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index dd37f84..3ef93e4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -18,6 +18,7 @@
   'use strict';
   Polymer({
     is: 'gr-plugin-popup',
+    _legacyUndefinedCheck: true,
     get opened() {
       return this.$.overlay.opened;
     },
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
index e47ba15..c9486ae 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
@@ -25,6 +25,7 @@
   <script>
     Polymer({
       is: 'gr-plugin-repo-command',
+      _legacyUndefinedCheck: true,
       properties: {
         title: String,
         repoName: String,
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
index e6e8fa5..496d0e7 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
@@ -37,6 +37,7 @@
   <script>
     Polymer({
       is: 'gr-custom-plugin-header',
+      _legacyUndefinedCheck: true,
       properties: {
         logoUrl: String,
         title: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index fcc99aa..feeb7df 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-info',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 41595a9..fe36a86 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-agreements-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _agreements: Array,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 7d109633..3909c99 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-table-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       displayedColumns: {
@@ -41,8 +42,9 @@
      * @return {!Array<string>}
      */
     _getDisplayedColumns() {
-      return Polymer.dom(this.root)
-          .querySelectorAll('.checkboxContainer input:not([name=number])')
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      return Array.from(Polymer.dom(this.root)
+          .querySelectorAll('.checkboxContainer input:not([name=number])'))
           .filter(checkbox => checkbox.checked)
           .map(checkbox => checkbox.name);
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index c771332..a59b886 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-cla-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Object,
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 86350f9..37bce08 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-edit-preferences',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index d08cc90..71d75cc 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-email-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index 78025d1..7348067 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-gpg-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index 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-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index 0f43563..4de24aa 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-group-list',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _groups: Array,
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index b3d1396..cda6da7 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-http-password',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _username: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index da6ab28..0d053f7 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-identities',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _identities: Object,
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index aa83bf7..8587338 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-menu-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       menuItems: Array,
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index c6cd578..6b4ee18 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-registration-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when account details are changed.
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
index b61c7e0..30a3801 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
@@ -18,13 +18,13 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <dom-module id="gr-settings-item">
-  <style>
-    :host {
-      display: block;
-      margin-bottom: 2em;
-    }
-  </style>
   <template>
+    <style>
+      :host {
+        display: block;
+        margin-bottom: 2em;
+      }
+    </style>
     <h2 id="[[anchor]]">[[title]]</h2>
     <slot></slot>
   </template>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index 0ee1b28..dc1aa93 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-settings-item',
+    _legacyUndefinedCheck: true,
     properties: {
       anchor: String,
       title: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
index 3b47190..f64d898 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
@@ -19,9 +19,9 @@
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
 
 <dom-module id="gr-settings-menu-item">
-  <style include="shared-styles"></style>
-  <style include="gr-page-nav-styles"></style>
   <template>
+    <style include="shared-styles"></style>
+    <style include="gr-page-nav-styles"></style>
     <div class="navStyles">
       <li><a href$="[[href]]">[[title]]</a></li>
     </div>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index 38147cd..2a56b09 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-settings-menu-item',
+    _legacyUndefinedCheck: true,
     properties: {
       href: String,
       title: String,
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 029ce88..21bf649 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -27,6 +27,7 @@
 <link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
@@ -194,6 +195,28 @@
               </gr-select>
             </span>
           </section>
+          <section hidden$="[[!_localPrefs.default_base_for_merges]]">
+            <span class="title">Default Base For Merges</span>
+            <span class="value">
+              <gr-select
+                  bind-value="{{_localPrefs.default_base_for_merges}}">
+                <select>
+                  <option value="AUTO_MERGE">Auto Merge</option>
+                  <option value="FIRST_PARENT">First Parent</option>
+                </select>
+              </gr-select>
+            </span>
+          </section>
+          <section>
+            <span class="title">Show Relative Dates In Changes Table</span>
+            <span class="value">
+              <input
+                  id="relativeDateInChangeTable"
+                  type="checkbox"
+                  checked$="[[_localPrefs.relative_date_in_change_table]]"
+                  on-change="_handleRelativeDateInChangeTable">
+            </span>
+          </section>
           <section>
             <span class="title">Diff view</span>
             <span class="value">
@@ -259,109 +282,9 @@
           Diff Preferences
         </h2>
         <fieldset id="diffPreferences">
-          <section>
-            <span class="title">Context</span>
-            <span class="value">
-              <gr-select bind-value="{{_diffPrefs.context}}">
-                <select>
-                  <option value="3">3 lines</option>
-                  <option value="10">10 lines</option>
-                  <option value="25">25 lines</option>
-                  <option value="50">50 lines</option>
-                  <option value="75">75 lines</option>
-                  <option value="100">100 lines</option>
-                  <option value="-1">Whole file</option>
-                </select>
-              </gr-select>
-            </span>
-          </section>
-          <section>
-            <span class="title">Fit to screen</span>
-            <span class="value">
-              <input
-                  id="diffLineWrapping"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.line_wrapping]]"
-                  on-change="_handleDiffLineWrappingChanged">
-            </span>
-          </section>
-          <section id="columnsPref" hidden$="[[_diffPrefs.line_wrapping]]">
-            <span class="title">Diff width</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.line_length}}">
-            </span>
-          </section>
-          <section>
-            <span class="title">Tab width</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.tab_size}}">
-            </span>
-          </section>
-          <section hidden$="[[!_diffPrefs.font_size]]">
-            <span class="title">Font size</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.font_size}}">
-            </span>
-          </section>
-          <section>
-            <span class="title">Show tabs</span>
-            <span class="value">
-              <input
-                  id="diffShowTabs"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.show_tabs]]"
-                  on-change="_handleDiffShowTabsChanged">
-            </span>
-          </section>
-          <section>
-            <span class="title">Show trailing whitespace</span>
-            <span class="value">
-              <input
-                  id="showTrailingWhitespace"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.show_whitespace_errors]]"
-                  on-change="_handleShowTrailingWhitespaceChanged">
-            </span>
-          </section>
-          <section>
-            <span class="title">Syntax highlighting</span>
-            <span class="value">
-              <input
-                  id="diffSyntaxHighlighting"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.syntax_highlighting]]"
-                  on-change="_handleDiffSyntaxHighlightingChanged">
-            </span>
-          </section>
-          <section>
-          <div class="pref">
-            <span class="title">Ignore Whitespace</span>
-            <span class="value">
-              <gr-select bind-value="{{_diffPrefs.ignore_whitespace}}">
-                <select>
-                  <option value="IGNORE_NONE">None</option>
-                  <option value="IGNORE_TRAILING">Trailing</option>
-                  <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
-                  <option value="IGNORE_ALL">All</option>
-                </select>
-              </gr-select>
-            </span>
-          </div>
+          <gr-diff-preferences
+              id="diffPrefs"
+              has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
           <gr-button
               id="saveDiffPrefs"
               on-tap="_handleSaveDiffPreferences"
@@ -461,10 +384,12 @@
               disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
               on-tap="_handleAddEmailButton">Send verification</gr-button>
         </fieldset>
-        <h2 id="HTTPCredentials">HTTP Credentials</h2>
-        <fieldset>
-          <gr-http-password id="httpPass"></gr-http-password>
-        </fieldset>
+        <div hidden$="[[!_showHttpAuth(_serverConfig)]]">
+          <h2 id="HTTPCredentials">HTTP Credentials</h2>
+          <fieldset>
+            <gr-http-password id="httpPass"></gr-http-password>
+          </fieldset>
+        </div>
         <div hidden$="[[!_serverConfig.sshd]]">
           <h2
               id="SSHKeys"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 0ce8ce0..1f76a68 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -25,9 +25,11 @@
     'diff_view',
     'publish_comments_on_push',
     'work_in_progress_by_default',
+    'default_base_for_merges',
     'signed_off_by',
     'email_format',
     'size_bar_in_change_table',
+    'relative_date_in_change_table',
   ];
 
   const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
@@ -38,8 +40,14 @@
 
   const RELOAD_MESSAGE = 'Reloading...';
 
+  const HTTP_AUTH = [
+    'HTTP',
+    'HTTP_LDAP',
+  ];
+
   Polymer({
     is: 'gr-settings-view',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the title of the page should change.
@@ -64,8 +72,6 @@
       },
       _accountNameMutable: Boolean,
       _accountInfoChanged: Boolean,
-      /** @type {?} */
-      _diffPrefs: Object,
       _changeTableColumnsNotDisplayed: Array,
       /** @type {?} */
       _localPrefs: {
@@ -92,10 +98,8 @@
         type: Boolean,
         value: false,
       },
-      _diffPrefsChanged: {
-        type: Boolean,
-        value: false,
-      },
+      /** @type {?} */
+      _diffPrefsChanged: Boolean,
       /** @type {?} */
       _editPrefsChanged: Boolean,
       _menuChanged: {
@@ -149,7 +153,6 @@
 
     observers: [
       '_handlePrefsChanged(_localPrefs.*)',
-      '_handleDiffPrefsChanged(_diffPrefs.*)',
       '_handleMenuChanged(_localMenu.splices)',
       '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
     ],
@@ -166,6 +169,7 @@
         this.$.httpPass.loadData(),
         this.$.identities.loadData(),
         this.$.editPrefs.loadData(),
+        this.$.diffPrefs.loadData(),
       ];
 
       promises.push(this.$.restAPI.getPreferences().then(prefs => {
@@ -176,10 +180,6 @@
         this._cloneChangeTableColumns();
       }));
 
-      promises.push(this.$.restAPI.getDiffPreferences().then(prefs => {
-        this._diffPrefs = prefs;
-      }));
-
       promises.push(this.$.restAPI.getConfig().then(config => {
         this._serverConfig = config;
         const configPromises = [];
@@ -277,9 +277,9 @@
       this._prefsChanged = true;
     },
 
-    _handleDiffPrefsChanged() {
-      if (this._isLoading()) { return; }
-      this._diffPrefsChanged = true;
+    _handleRelativeDateInChangeTable() {
+      this.set('_localPrefs.relative_date_in_change_table',
+          this.$.relativeDateInChangeTable.checked);
     },
 
     _handleShowSizeBarsInFileListChanged() {
@@ -318,24 +318,6 @@
       });
     },
 
-    _handleDiffLineWrappingChanged() {
-      this.set('_diffPrefs.line_wrapping', this.$.diffLineWrapping.checked);
-    },
-
-    _handleDiffShowTabsChanged() {
-      this.set('_diffPrefs.show_tabs', this.$.diffShowTabs.checked);
-    },
-
-    _handleShowTrailingWhitespaceChanged() {
-      this.set('_diffPrefs.show_whitespace_errors',
-          this.$.showTrailingWhitespace.checked);
-    },
-
-    _handleDiffSyntaxHighlightingChanged() {
-      this.set('_diffPrefs.syntax_highlighting',
-          this.$.diffSyntaxHighlighting.checked);
-    },
-
     _handleSaveChangeTable() {
       this.set('prefs.change_table', this._localChangeTableColumns);
       this.set('prefs.legacycid_in_change_table', this._showNumber);
@@ -346,10 +328,7 @@
     },
 
     _handleSaveDiffPreferences() {
-      return this.$.restAPI.saveDiffPreferences(this._diffPrefs)
-          .then(() => {
-            this._diffPrefsChanged = false;
-          });
+      this.$.diffPrefs.save();
     },
 
     _handleSaveEditPreferences() {
@@ -440,5 +419,15 @@
         window.location.reload();
       }, 1);
     },
+
+    _showHttpAuth(config) {
+      if (config && config.auth &&
+          config.auth.git_basic_auth_policy) {
+        return HTTP_AUTH.includes(
+            config.auth.git_basic_auth_policy.toUpperCase());
+      }
+
+      return false;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index f47816f..506c6af 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -43,7 +43,6 @@
     let element;
     let account;
     let preferences;
-    let diffPreferences;
     let config;
     let sandbox;
 
@@ -88,6 +87,8 @@
         diff_view: 'UNIFIED_DIFF',
         email_strategy: 'ENABLED',
         email_format: 'HTML_PLAINTEXT',
+        default_base_for_merges: 'FIRST_PARENT',
+        relative_date_in_change_table: false,
         size_bar_in_change_table: true,
 
         my: [
@@ -96,31 +97,12 @@
         ],
         change_table: [],
       };
-      diffPreferences = {
-        context: 10,
-        tab_size: 8,
-        font_size: 12,
-        line_length: 100,
-        cursor_blink_rate: 0,
-        line_wrapping: false,
-        intraline_difference: true,
-        show_line_endings: true,
-        show_tabs: true,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-        auto_hide_diff_table_header: true,
-        theme: 'DEFAULT',
-        ignore_whitespace: 'IGNORE_NONE',
-      };
       config = {auth: {editable_account_fields: []}};
 
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getAccount() { return Promise.resolve(account); },
         getPreferences() { return Promise.resolve(preferences); },
-        getDiffPreferences() {
-          return Promise.resolve(diffPreferences);
-        },
         getWatchedProjects() {
           return Promise.resolve([]);
         },
@@ -168,6 +150,11 @@
           .firstElementChild.bindValue, preferences.email_strategy);
       assert.equal(valueOf('Email format', 'preferences')
           .firstElementChild.bindValue, preferences.email_format);
+      assert.equal(valueOf('Default Base For Merges', 'preferences')
+          .firstElementChild.bindValue, preferences.default_base_for_merges);
+      assert.equal(
+          valueOf('Show Relative Dates In Changes Table', 'preferences')
+              .firstElementChild.checked, false);
       assert.equal(valueOf('Diff view', 'preferences')
           .firstElementChild.bindValue, preferences.diff_view);
       assert.equal(valueOf('Show size bars in file list', 'preferences')
@@ -261,56 +248,6 @@
       });
     });
 
-    test('diff preferences', done => {
-      // Rendered with the expected preferences selected.
-      assert.equal(valueOf('Context', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.context);
-      assert.equal(valueOf('Diff width', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.line_length);
-      assert.equal(valueOf('Tab width', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.tab_size);
-      assert.equal(valueOf('Font size', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.font_size);
-      assert.equal(valueOf('Show tabs', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.show_tabs);
-      assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.show_whitespace_errors);
-      assert.equal(valueOf('Fit to screen', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.line_wrapping);
-
-      assert.isFalse(element._diffPrefsChanged);
-
-      const showTabsCheckbox = valueOf('Show tabs', 'diffPreferences')
-          .firstElementChild;
-      showTabsCheckbox.checked = false;
-      element._handleDiffShowTabsChanged();
-
-      assert.isTrue(element._diffPrefsChanged);
-
-      stub('gr-rest-api-interface', {
-        saveDiffPreferences(prefs) {
-          assert.equal(prefs.show_tabs, false);
-          return Promise.resolve();
-        },
-      });
-
-      // Save the change.
-      element._handleSaveDiffPreferences().then(() => {
-        assert.isFalse(element._diffPrefsChanged);
-        done();
-      });
-    });
-
-    test('columns input is hidden with fit to scsreen is selected', () => {
-      assert.isFalse(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.diffLineWrapping);
-      assert.isTrue(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.diffLineWrapping);
-      assert.isFalse(element.$.columnsPref.hidden);
-    });
-
     test('menu', done => {
       assert.isFalse(element._menuChanged);
       assert.isFalse(element._prefsChanged);
@@ -470,6 +407,46 @@
       assert.isTrue(overlayOpen.called);
     });
 
+    test('_showHttpAuth', () => {
+      let serverConfig;
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'HTTP',
+        },
+      };
+
+      assert.isTrue(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'HTTP_LDAP',
+        },
+      };
+
+      assert.isTrue(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'LDAP',
+        },
+      };
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'OAUTH',
+        },
+      };
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+
+      serverConfig = {};
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+    });
+
     suite('_getFilterDocsLink', () => {
       test('with http: docs base URL', () => {
         const base = 'http://example.com/';
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index 874173a..4c423e8 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-ssh-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 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/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index ebf61db..151eb8b 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -27,6 +27,7 @@
 
   Polymer({
     is: 'gr-watched-projects-editor',
+    _legacyUndefinedCheck: true,
 
     properties: {
       hasUnsavedChanges: {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 880cbc0..827e33b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -20,6 +20,7 @@
 
   Polymer({
     is: 'gr-account-chip',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired to indicate a key was pressed while this chip was focused.
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 5b1b975..7983fad 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-label',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index faaf9c3..03967f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-account-link',
+    _legacyUndefinedCheck: true,
 
     properties: {
       additionalText: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index e7c8b2c..ec7b6eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-alert',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the action button is pressed.
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 2e55010..1af629d2 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-autocomplete-dropdown',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the dropdown is closed.
@@ -162,7 +163,9 @@
     _resetCursorStops() {
       if (this.suggestions.length > 0) {
         Polymer.dom.flush();
-        this._suggestionEls = this.$.suggestions.querySelectorAll('li');
+        // Polymer2: querySelectorAll returns NodeList instead of Array.
+        this._suggestionEls = Array.from(
+            this.$.suggestions.querySelectorAll('li'));
       } else {
         this._suggestionEls = [];
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 4b447d1..a878174 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -29,7 +29,7 @@
         display: none;
       }
       .searchIcon.showSearchIcon {
-        display: initial;
+        display: inline-block;
       }
       iron-icon {
         margin: 0 .25em;
@@ -68,7 +68,6 @@
         no-label-float
         id="input"
         class$="[[_computeClass(borderless)]]"
-        is="iron-input"
         disabled$="[[disabled]]"
         value="{{text}}"
         placeholder="[[placeholder]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 4efa7ad..76e14a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-autocomplete',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
@@ -210,7 +211,8 @@
     },
 
     get _inputElement() {
-      return this.$.input;
+      // Polymer2: this.$ can be undefined when this is first evaluated.
+      return this.$ && this.$.input;
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index f32e940b..2435e58 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-avatar',
+    _legacyUndefinedCheck: true,
 
     properties: {
       account: {
@@ -41,25 +42,30 @@
 
     attached() {
       Promise.all([
-        this.$.restAPI.getConfig(),
+        this._getConfig(),
         Gerrit.awaitPluginsLoaded(),
       ]).then(([cfg]) => {
         this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-        if (this._hasAvatars && this.account) {
-          // src needs to be set if avatar becomes visible
-          this._updateAvatarURL();
-        } else {
-          this.hidden = true;
-        }
+
+        this._updateAvatarURL();
       });
     },
 
+    _getConfig() {
+      return this.$.restAPI.getConfig();
+    },
+
     _accountChanged(account) {
       this._updateAvatarURL();
     },
 
     _updateAvatarURL() {
-      if (this.hidden || !this._hasAvatars) { return; }
+      if (!this._hasAvatars || !this.account) {
+        this.hidden = true;
+        return;
+      }
+      this.hidden = false;
+
       const url = this._buildAvatarURL(this.account);
       if (url) {
         this.style.backgroundImage = 'url("' + url + '")';
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index f137c7f..5ce17c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -35,14 +35,17 @@
 <script>
   suite('gr-avatar tests', () => {
     let element;
+    let sandbox;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({plugin: {has_avatars: true}}); },
-      });
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
     });
 
+    teardown(() => {
+      sandbox.restore();
+    });
+
     test('methods', () => {
       assert.equal(element._buildAvatarURL(
           {
@@ -94,22 +97,32 @@
             ],
           }),
           '/accounts/123/avatar?s=16');
+      assert.equal(element._buildAvatarURL(undefined), '');
     });
 
     test('dom for existing account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({plugin: {has_avatars: true}});
+      });
+
       element.imageSize = 64;
       element.account = {
         _account_id: 123,
       };
+
       assert.strictEqual(element.style.backgroundImage, '');
+
       // Emulate plugins loaded.
       Gerrit._setPluginsPending([]);
-      return Promise.all([
+
+      Promise.all([
         element.$.restAPI.getConfig(),
         Gerrit.awaitPluginsLoaded(),
       ]).then(() => {
         assert.isFalse(element.hasAttribute('hidden'));
+
         assert.isTrue(
             element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
       });
@@ -117,10 +130,57 @@
 
     test('dom for non available account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
-      element.account = null;
-      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({plugin: {has_avatars: true}});
+      });
+
       // Emulate plugins loaded.
       Gerrit._setPluginsPending([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+
+        assert.strictEqual(element.style.backgroundImage, '');
+      });
+    });
+
+    test('avatar config not set and account not set', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({});
+      });
+
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+      });
+    });
+
+    test('avatar config not set and account set', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({});
+      });
+
+      element.imageSize = 64;
+      element.account = {
+        _account_id: 123,
+      };
+
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+
       return Promise.all([
         element.$.restAPI.getConfig(),
         Gerrit.awaitPluginsLoaded(),
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index fe18180..cdf617f 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -94,7 +94,9 @@
       }
 
       /* Styles for the optional down arrow */
-      :host:not([down-arrow]) .downArrow {display: none; }
+      :host(:not([down-arrow])) .downArrow {
+        display: none;
+      }
       :host([down-arrow]) .downArrow {
         border-top: .36em solid #ccc;
         border-left: .36em solid transparent;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index ca6705e..5988cde 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-button',
+    _legacyUndefinedCheck: true,
 
     properties: {
       tooltip: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 3c46d1b..44f8c00 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-change-star',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when star state is toggled.
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
index e256bf3..99ddff1 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
@@ -68,7 +68,7 @@
         background-color: transparent;
         padding: .1em;
       }
-      :host:not([flat]) .chip {
+      :host(:not([flat])) .chip {
         color: white;
       }
     </style>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 8efd309..70c5d72 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -26,14 +26,15 @@
   };
 
   const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
-      'It will not appear in dashboards, and email notifications will be ' +
-      'silenced until the review is started.';
+      'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+      'and email notifications will be silenced until the review is started.';
 
   const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
       'current reviewers (or anyone with "View Private Changes" permission).';
 
   Polymer({
     is: 'gr-change-status',
+    _legacyUndefinedCheck: true,
 
     properties: {
       flat: {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index 11fce6d..cf98b42c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-comment-thread',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the thread should be discarded.
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index a576583..b5481a9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -33,6 +33,7 @@
 
   Polymer({
     is: 'gr-comment',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the create fix comment action is triggered.
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index b86b72a..4ac059d 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-confirm-delete-comment-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index cabee36..550f1df 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-copy-clipboard',
+    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index f750cd2..36e6a7b 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -24,6 +24,7 @@
 
   Polymer({
     is: 'gr-cursor-manager',
+    _legacyUndefinedCheck: true,
 
     properties: {
       stops: {
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 1090fea..481dd2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -20,7 +20,6 @@
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-<script src="../../../bower_components/moment/moment.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-date-formatter">
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 3417a0d..4d7f2bb 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -33,6 +33,7 @@
 
   Polymer({
     is: 'gr-date-formatter',
+    _legacyUndefinedCheck: true,
 
     properties: {
       dateStr: {
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index 6163b09..b8b2af4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-dialog',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the confirm button is pressed.
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
new file mode 100644
index 0000000..d7bc704
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
@@ -0,0 +1,163 @@
+<!--
+@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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-select/gr-select.html">
+
+<dom-module id="gr-diff-preferences">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles"></style>
+    <div id="diffPreferences" class="gr-form-styles">
+      <section>
+        <span class="title">Context</span>
+        <span class="value">
+          <gr-select
+              id="contextSelect"
+              bind-value="{{diffPrefs.context}}">
+            <select
+                on-keypress="_handleDiffPrefsChanged"
+                on-change="_handleDiffPrefsChanged">
+              <option value="3">3 lines</option>
+              <option value="10">10 lines</option>
+              <option value="25">25 lines</option>
+              <option value="50">50 lines</option>
+              <option value="75">75 lines</option>
+              <option value="100">100 lines</option>
+              <option value="-1">Whole file</option>
+            </select>
+          </gr-select>
+        </span>
+      </section>
+      <section>
+        <span class="title">Fit to screen</span>
+        <span class="value">
+          <input
+              id="lineWrappingInput"
+              type="checkbox"
+              checked$="[[diffPrefs.line_wrapping]]"
+              on-change="_handleLineWrappingTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Diff width</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="columnsInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.line_length}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Tab width</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="tabSizeInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.tab_size}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section hidden$="[[!diffPrefs.font_size]]">
+        <span class="title">Font size</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="fontSizeInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.font_size}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Show tabs</span>
+        <span class="value">
+          <input
+              id="showTabsInput"
+              type="checkbox"
+              checked$="[[diffPrefs.show_tabs]]"
+              on-change="_handleShowTabsTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Show trailing whitespace</span>
+        <span class="value">
+          <input
+              id="showTrailingWhitespaceInput"
+              type="checkbox"
+              checked$="[[diffPrefs.show_whitespace_errors]]"
+              on-change="_handleShowTrailingWhitespaceTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Syntax highlighting</span>
+        <span class="value">
+          <input
+              id="syntaxHighlightInput"
+              type="checkbox"
+              checked$="[[diffPrefs.syntax_highlighting]]"
+              on-change="_handleSyntaxHighlightTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Automatically mark viewed files reviewed</span>
+        <span class="value">
+          <input
+              id="automaticReviewInput"
+              type="checkbox"
+              checked$="[[diffPrefs.manual_review]]"
+              on-change="_handleAutomaticReviewTap">
+        </span>
+      </section>
+      <section>
+        <div class="pref">
+          <span class="title">Ignore Whitespace</span>
+          <span class="value">
+            <gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
+              <select
+                  on-keypress="_handleDiffPrefsChanged"
+                  on-change="_handleDiffPrefsChanged">
+                <option value="IGNORE_NONE">None</option>
+                <option value="IGNORE_TRAILING">Trailing</option>
+                <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+                <option value="IGNORE_ALL">All</option>
+              </select>
+            </gr-select>
+          </span>
+        </div>
+      </section>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-diff-preferences.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
new file mode 100644
index 0000000..eb99d7a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-preferences',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      hasUnsavedChanges: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+
+      /** @type {?} */
+      diffPrefs: Object,
+    },
+
+    loadData() {
+      return this.$.restAPI.getDiffPreferences().then(prefs => {
+        this.diffPrefs = prefs;
+      });
+    },
+
+    _handleDiffPrefsChanged() {
+      this.hasUnsavedChanges = true;
+    },
+
+    _handleLineWrappingTap() {
+      this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleShowTabsTap() {
+      this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleShowTrailingWhitespaceTap() {
+      this.set('diffPrefs.show_whitespace_errors',
+          this.$.showTrailingWhitespaceInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleSyntaxHighlightTap() {
+      this.set('diffPrefs.syntax_highlighting',
+          this.$.syntaxHighlightInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleAutomaticReviewTap() {
+      this.set('diffPrefs.manual_review',
+          this.$.automaticReviewInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    save() {
+      return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
+        this.hasUnsavedChanges = false;
+      });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
new file mode 100644
index 0000000..511737f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-preferences</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-diff-preferences.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-diff-preferences></gr-diff-preferences>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-diff-preferences tests', () => {
+    let element;
+    let sandbox;
+    let diffPreferences;
+
+    function valueOf(title, fieldsetid) {
+      const sections = element.$[fieldsetid].querySelectorAll('section');
+      let titleEl;
+      for (let i = 0; i < sections.length; i++) {
+        titleEl = sections[i].querySelector('.title');
+        if (titleEl.textContent.trim() === title) {
+          return sections[i].querySelector('.value');
+        }
+      }
+    }
+
+    setup(() => {
+      diffPreferences = {
+        context: 10,
+        line_wrapping: false,
+        line_length: 100,
+        tab_size: 8,
+        font_size: 12,
+        show_tabs: true,
+        show_whitespace_errors: true,
+        syntax_highlighting: true,
+        manual_review: false,
+        ignore_whitespace: 'IGNORE_NONE',
+      };
+
+      stub('gr-rest-api-interface', {
+        getDiffPreferences() {
+          return Promise.resolve(diffPreferences);
+        },
+      });
+
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+      return element.loadData();
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders', () => {
+      // Rendered with the expected preferences selected.
+      assert.equal(valueOf('Context', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.context);
+      assert.equal(valueOf('Fit to screen', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.line_wrapping);
+      assert.equal(valueOf('Diff width', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.line_length);
+      assert.equal(valueOf('Tab width', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.tab_size);
+      assert.equal(valueOf('Font size', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.font_size);
+      assert.equal(valueOf('Show tabs', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.show_tabs);
+      assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.show_whitespace_errors);
+      assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.syntax_highlighting);
+      assert.equal(
+          valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
+              .firstElementChild.checked, diffPreferences.manual_review);
+      assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+
+      assert.isFalse(element.hasUnsavedChanges);
+    });
+
+    test('save changes', () => {
+      sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
+          .returns(Promise.resolve());
+      const showTrailingWhitespaceCheckbox =
+          valueOf('Show trailing whitespace', 'diffPreferences')
+          .firstElementChild;
+      showTrailingWhitespaceCheckbox.checked = false;
+      element._handleShowTrailingWhitespaceTap();
+
+      assert.isTrue(element.hasUnsavedChanges);
+
+      // Save the change.
+      return element.save().then(() => {
+        assert.isFalse(element.hasUnsavedChanges);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index ca77a30..ed7c2cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-download-commands',
+    _legacyUndefinedCheck: true,
     properties: {
       commands: Array,
       _loggedIn: {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 40d8811..bcf3729 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -47,6 +47,7 @@
 
   Polymer({
     is: 'gr-dropdown-list',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the selected value changes
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 3d9d36b..6eb3108 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-dropdown',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a non-link dropdown item with the given ID is tapped.
@@ -280,7 +281,9 @@
      */
     _resetCursorStops() {
       Polymer.dom.flush();
-      this._listElements = Polymer.dom(this.root).querySelectorAll('li');
+      // Polymer2: querySelectorAll returns NodeList instead of Array.
+      this._listElements = Array.from(
+          Polymer.dom(this.root).querySelectorAll('li'));
     },
 
     _computeHasTooltip(tooltip) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index f567d39..827bb71 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-editable-content',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the save button is pressed.
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index 3514492..f23afea 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-editable-label',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when the value is changed.
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 2c32709..87cd4b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-fixed-panel',
+    _legacyUndefinedCheck: true,
 
     properties: {
       floatingDisabled: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index 4e68d42..8836574 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-formatted-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       content: {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index f9c1da1..498c590 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -27,6 +27,7 @@
 
   Polymer({
     is: 'gr-hovercard',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /**
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 4ea8cc7..1f885af 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -20,29 +20,29 @@
 <iron-iconset-svg name="gr-icons" size="24">
   <svg>
     <defs>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
-      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
       <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"/></g>
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..21fd9eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -28,6 +28,7 @@
     POST_REVERT: 'postrevert',
     ANNOTATE_DIFF: 'annotatediff',
     ADMIN_MENU_LINKS: 'admin-menu-links',
+    HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
   };
 
   const Element = {
@@ -37,6 +38,7 @@
 
   Polymer({
     is: 'gr-js-api-interface',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _elements: {
@@ -69,6 +71,9 @@
           case EventType.LABEL_CHANGE:
             this._handleLabelChange(detail);
             break;
+          case EventType.HIGHLIGHTJS_LOADED:
+            this._handleHighlightjsLoaded(detail);
+            break;
           default:
             console.warn('handleEvent called with unsupported event type:',
                 type);
@@ -188,6 +193,16 @@
       }
     },
 
+    _handleHighlightjsLoaded(detail) {
+      for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
+        try {
+          cb(detail.hljs);
+        } catch (err) {
+          console.error(err);
+        }
+      }
+    },
+
     modifyRevertMsg(change, revertMsg, origMsg) {
       for (const cb of this._getEventCallbacks(EventType.REVERT)) {
         try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 54c283d..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');
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-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
index c18f753..eb0c7e0 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -38,6 +38,10 @@
     return getRestApi().getVersion();
   };
 
+  GrPluginRestApi.prototype.invalidateReposCache = function() {
+    getRestApi().invalidateReposCache();
+  };
+
   /**
    * Fetch and return native browser REST API Response.
    * @param {string} method HTTP Method (GET, POST, etc)
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 8bf4a2d..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();
   };
 
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 40edc28..2036f2a 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-label-info',
+    _legacyUndefinedCheck: true,
 
     properties: {
       labelInfo: Object,
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index 0de0881..c437885 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-label',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index 54b38eb..a892522 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-labeled-autocomplete',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a value is chosen.
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
index f70aff4..4137485 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -15,7 +15,11 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-lib-loader">
+  <template>
+    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+  </template>
   <script src="gr-lib-loader.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index ef8c112..ba0ab1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-lib-loader',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _hljsState: {
@@ -82,6 +83,9 @@
     _onHLJSLibLoaded() {
       const lib = this._getHighlightLib();
       this._hljsState.loading = false;
+      this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
+        hljs: lib,
+      });
       for (const cb of this._hljsState.callbacks) {
         cb(lib);
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index 0dc3a7d..44a8791 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -26,6 +26,7 @@
 
   Polymer({
     is: 'gr-limited-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       /** The un-truncated text to display. */
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index f8f29b8..8388a07 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-linked-chip',
+    _legacyUndefinedCheck: true,
 
     properties: {
       href: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 530da02..b1bd5cd 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-linked-text',
+    _legacyUndefinedCheck: true,
 
     properties: {
       removeZeroWidthSpace: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 8b83eb3..53d05e1 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -21,6 +21,7 @@
 
   Polymer({
     is: 'gr-list-view',
+    _legacyUndefinedCheck: true,
 
     properties: {
       createNew: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index 6df04a2..c167b3b 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -23,6 +23,7 @@
 
   Polymer({
     is: 'gr-overlay',
+    _legacyUndefinedCheck: true,
 
     /**
      * Fired when a fullscreen overlay is closed
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index 9ccff600..0962540 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-page-nav',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _headerHeight: Number,
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index e2298c3..2fccc8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -22,6 +22,7 @@
 
   Polymer({
     is: 'gr-repo-branch-picker',
+    _legacyUndefinedCheck: true,
 
     properties: {
       repo: {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index d9b0cbf..4b66a6e 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
@@ -183,6 +183,7 @@
 
   Polymer({
     is: 'gr-rest-api-interface',
+    _legacyUndefinedCheck: true,
 
     behaviors: [
       Gerrit.PathListBehavior,
@@ -353,7 +354,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 +1322,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 +1353,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) {
@@ -1605,7 +1610,7 @@
       this._invalidateSharedFetchPromisesPrefix('/groups/?');
     },
 
-    invalidateReposCache(filter, reposPerPage, opt_offset) {
+    invalidateReposCache() {
       this._invalidateSharedFetchPromisesPrefix('/projects/?');
     },
 
@@ -1886,16 +1891,21 @@
       });
     },
 
-    getChangesWithSameTopic(topic) {
+    getChangesWithSameTopic(topic, changeNum) {
       const options = this.listChangesOptionsToHex(
           this.ListChangesOption.LABELS,
           this.ListChangesOption.CURRENT_REVISION,
           this.ListChangesOption.CURRENT_COMMIT,
           this.ListChangesOption.DETAILED_LABELS
       );
+      const query = [
+        'status:open',
+        '-change:' + changeNum,
+        `topic:"${topic}"`,
+      ].join(' ');
       const params = {
         O: options,
-        q: 'status:open topic:' + topic,
+        q: query,
       };
       return this._fetchJSON({
         url: '/changes/',
@@ -2501,7 +2511,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 => {
@@ -2661,12 +2673,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;
           });
     },
@@ -2696,12 +2708,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 667f24c..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)
@@ -956,7 +956,7 @@
 
       element._cache.set(url, {});
 
-      element.invalidateReposCache('test', 25);
+      element.invalidateReposCache();
 
       assert.isUndefined(element._sharedFetchPromises[url]);
 
@@ -1043,7 +1043,7 @@
 
       element._cache.set(url, {});
 
-      element.invalidateGroupsCache('test', 25);
+      element.invalidateGroupsCache();
 
       assert.isUndefined(element._sharedFetchPromises[url]);
 
@@ -1151,12 +1151,12 @@
       test('_getChangeDetail passes params to ETags decorator', () => {
         const changeNum = 4321;
         element._projectLookup[changeNum] = 'test';
-        const params = {foo: 'bar'};
         const expectedUrl =
-            window.CANONICAL_PATH + '/changes/test~4321/detail?foo=bar';
+            window.CANONICAL_PATH + '/changes/test~4321/detail?'+
+            '0=5&1=1&2=6&3=7&4=1&5=4';
         sandbox.stub(element._etags, 'getOptions');
         sandbox.stub(element._etags, 'collect');
-        return element._getChangeDetail(changeNum, params).then(() => {
+        return element._getChangeDetail(changeNum, '516714').then(() => {
           assert.isTrue(element._etags.getOptions.calledWithExactly(
               expectedUrl));
           assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
@@ -1169,7 +1169,7 @@
             .returns(Promise.resolve(''));
         sandbox.stub(element, '_fetchRawJSON')
             .returns(Promise.resolve({ok: false, status: 500}));
-        return element._getChangeDetail(123, {}, errFn).then(() => {
+        return element._getChangeDetail(123, '516714', errFn).then(() => {
           assert.isTrue(errFn.called);
         });
       });
@@ -1185,7 +1185,7 @@
           parsed: mockResponse,
           raw: JSON.stringify(mockResponse),
         }));
-        return element._getChangeDetail(1).then(() => {
+        return element._getChangeDetail(1, '516714').then(() => {
           assert.equal(Object.keys(element._projectLookup).length, 1);
           assert.equal(element._projectLookup[1], 'test');
         });
@@ -1216,7 +1216,7 @@
             ok: true,
           }));
 
-          return element._getChangeDetail(123, {}).then(detail => {
+          return element._getChangeDetail(123, '516714').then(detail => {
             assert.isFalse(getPayloadSpy.called);
             assert.isTrue(collectSpy.calledOnce);
             const cachedResponse = element._etags.getCachedPayload(requestUrl);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index 0a1b14e..05c2cee 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -153,6 +153,7 @@
 
       Polymer({
         is: 'mock-diff-response',
+        _legacyUndefinedCheck: true,
         properties: {
           diffResponse: {
             type: Object,
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index b732fa5..85e1a61 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-select',
+    _legacyUndefinedCheck: true,
     properties: {
       bindValue: {
         type: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 2c546cc..901b8ce 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-shell-command',
+    _legacyUndefinedCheck: true,
 
     properties: {
       command: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 62080a1..6146e0f 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -30,6 +30,7 @@
 
   Polymer({
     is: 'gr-storage',
+    _legacyUndefinedCheck: true,
 
     properties: {
       _lastCleanup: Number,
@@ -72,7 +73,7 @@
     },
 
     eraseEditableContentItem(key) {
-      this._storage.removeItem(key);
+      this._storage.removeItem(this._getEditableContentKey(key));
     },
 
     getPreferences() {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index b7d73d4..6b89af2 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -188,7 +188,7 @@
 
       // eraseEditableContentItem performs as expected.
       element.eraseEditableContentItem(key);
-      assert.isNotOk(element._storage.getItem(key));
+      assert.isNotOk(element._storage.getItem(computedKey));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index a3da7d8..7929fbe 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -54,6 +54,7 @@
 
   Polymer({
     is: 'gr-textarea',
+    _legacyUndefinedCheck: true,
 
     /**
      * @event bind-value-changed
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index c5de8f4..b46cafb 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-tooltip-content',
+    _legacyUndefinedCheck: true,
 
     properties: {
       title: {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index fb87b558..3e16beb 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -19,6 +19,7 @@
 
   Polymer({
     is: 'gr-tooltip',
+    _legacyUndefinedCheck: true,
 
     properties: {
       text: String,
diff --git a/polygerrit-ui/app/embed/gr-diff.html b/polygerrit-ui/app/embed/gr-diff.html
new file mode 100644
index 0000000..6aa9370
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff.html
@@ -0,0 +1,25 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<script>
+  // Needed for JSCompiler to understand it's global.
+  // eslint-disable-next-line no-unused-vars, prefer-const
+  let Gerrit = window.Gerrit || {};
+  window.Gerrit = Gerrit;
+</script>
+<link rel="import" href="../styles/themes/app-theme.html">
+<link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
+<link rel="import" href="../elements/diff/gr-diff-cursor/gr-diff-cursor.html">
diff --git a/polygerrit-ui/app/externs/BUILD b/polygerrit-ui/app/externs/BUILD
index fab3954..26ead9a 100644
--- a/polygerrit-ui/app/externs/BUILD
+++ b/polygerrit-ui/app/externs/BUILD
@@ -12,12 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
+
 package(
     default_visibility = ["//visibility:public"],
 )
 
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
-
 closure_js_library(
     name = "plugin",
     srcs = ["plugin.js"],
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 8bf104a..3012f7f 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,10 +1,8 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
+load("//tools/bzl:genrule2.bzl", "genrule2")
 load(
     "//tools/bzl:js.bzl",
-    "bower_component",
     "bundle_assets",
-    "js_component",
 )
 
 def polygerrit_bundle(name, srcs, outs, app):
@@ -24,6 +22,8 @@
         deps = [name + "_closure_lib"],
     )
 
+    # TODO(davido): Remove JSC_REFERENCE_BEFORE_DECLARE when this is fixed upstream:
+    # https://github.com/Polymer/polymer-resin/issues/7
     closure_js_library(
         name = name + "_closure_lib",
         srcs = [appName + ".js"],
@@ -32,6 +32,7 @@
         # and remove this supression
         suppress = [
             "JSC_JSDOC_MISSING_TYPE_WARNING",
+            "JSC_REFERENCE_BEFORE_DECLARE",
             "JSC_UNNECESSARY_ESCAPE",
             "JSC_UNUSED_LOCAL_ASSIGNMENT",
         ],
diff --git a/polygerrit-ui/app/samples/bind-parameters.html b/polygerrit-ui/app/samples/bind-parameters.html
index dc7a87a..a7eb39a 100644
--- a/polygerrit-ui/app/samples/bind-parameters.html
+++ b/polygerrit-ui/app/samples/bind-parameters.html
@@ -15,6 +15,7 @@
   <script>
     Polymer({
       is: 'my-bind-sample',
+      _legacyUndefinedCheck: true,
       properties: {
         computedExample: {
           type: String,
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index 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/samples/repo-command.html b/polygerrit-ui/app/samples/repo-command.html
index 67e528a..37aca04 100644
--- a/polygerrit-ui/app/samples/repo-command.html
+++ b/polygerrit-ui/app/samples/repo-command.html
@@ -29,6 +29,7 @@
   <script>
     Polymer({
       is: 'repo-command-low',
+      _legacyUndefinedCheck: true,
       attached() {
         console.log(this.repoName);
         console.log(this.config);
diff --git a/polygerrit-ui/app/samples/some-screen.html b/polygerrit-ui/app/samples/some-screen.html
index de29315..527ebce 100644
--- a/polygerrit-ui/app/samples/some-screen.html
+++ b/polygerrit-ui/app/samples/some-screen.html
@@ -38,6 +38,7 @@
   <script>
     Polymer({
       is: 'some-screen-main',
+      _legacyUndefinedCheck: true,
       properties: {
         rootUrl: String,
       },
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index b4ab21a..624992b 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -41,5 +41,39 @@
     }
     return '';
   };
+
+  /**
+   * Make the promise cancelable.
+   *
+   * Returns a promise with a `cancel()` method wrapped around `promise`.
+   * Calling `cancel()` will reject the returned promise with
+   * {isCancelled: true} synchronously. If the inner promise for a cancelled
+   * promise resolves or rejects this is ignored.
+   */
+  util.makeCancelable = promise => {
+    // True if the promise is either resolved or reject (possibly cancelled)
+    let isDone = false;
+
+    let rejectPromise;
+
+    const wrappedPromise = new Promise((resolve, reject) => {
+      rejectPromise = reject;
+      promise.then(val => {
+        if (!isDone) resolve(val);
+        isDone = true;
+      }, error => {
+        if (!isDone) reject(error);
+        isDone = true;
+      });
+    });
+
+    wrappedPromise.cancel = () => {
+      if (isDone) return;
+      rejectPromise({isCanceled: true});
+      isDone = true;
+    };
+    return wrappedPromise;
+  };
+
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index aeca48a..8f72216 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -55,8 +55,8 @@
       .cell {
         vertical-align: middle;
       }
-      th:not(.label),
-      .cell:not(.label) {
+      th:not(.label):not(.endpoint),
+      .cell:not(.label):not(.endpoint) {
         padding-right: 8px;
       }
       th.label {
@@ -128,8 +128,10 @@
       .star {
         width: 30px;
       }
-      .label {
+      .label, .endpoint {
         border-left: 1px solid var(--border-color);
+      }
+      .label {
         text-align: center;
         width: 3rem;
       }
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index fba3e97..a4a2ca4 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<style is="custom-style">
-:root {
+<custom-style><style is="custom-style">
+html {
   /* Following vars have LTS for plugin API. */
   --primary-text-color: #000;
   --header-background-color: #eee;
@@ -109,6 +109,8 @@
   --tooltip-text-color: #fff;
 
   --syntax-default-color: var(--primary-text-color);
+  --syntax-attribute-color: var(--primary-text-color);
+  --syntax-function-color: var(--primary-text-color);
   --syntax-meta-color: #FF1717;
   --syntax-keyword-color: #9E0069;
   --syntax-number-color: #164;
@@ -130,10 +132,13 @@
   --syntax-regexp-color: #FA8602;
   --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 {
+  html {
     --default-horizontal-margin: .7rem;
   }
 }
-</style>
+</style></custom-style>
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 6037a88..d5db416 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -1,5 +1,5 @@
 <dom-module id="dark-theme">
-  <style is="custom-style">
+  <custom-style><style is="custom-style">
     html {
       --primary-text-color: #e2e2e2;
       --view-background-color: #212121;
@@ -80,7 +80,9 @@
       --syntax-selector-attr-color: #80CBBF;
       --syntax-template-tag-color: #C792EA;
 
+      --reply-overlay-z-index: 1000;
+
       background-color: var(--view-background-color);
     }
-  </style>
+  </style></custom-style>
 </dom-module>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index 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 3b91650..d9e1238 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -71,6 +71,7 @@
     'change/gr-commit-info/gr-commit-info_test.html',
     'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
     'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
+    'change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html',
     'change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html',
     'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
     'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
@@ -107,7 +108,6 @@
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
     'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
-    'diff/gr-diff-preferences/gr-diff-preferences_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
     'diff/gr-diff-view/gr-diff-view_test.html',
@@ -161,6 +161,7 @@
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-dialog/gr-dialog_test.html',
+    'shared/gr-diff-preferences/gr-diff-preferences_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
     'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index ba685184..2f5df90 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -17,6 +17,7 @@
 import (
 	"archive/zip"
 	"bufio"
+	"bytes"
 	"compress/gzip"
 	"encoding/json"
 	"errors"
@@ -32,20 +33,16 @@
 	"regexp"
 	"strings"
 
-	"github.com/robfig/soy"
-	"github.com/robfig/soy/soyhtml"
 	"golang.org/x/tools/godoc/vfs/httpfs"
 	"golang.org/x/tools/godoc/vfs/zipfs"
 )
 
 var (
-	plugins  = flag.String("plugins", "", "comma seperated plugin paths to serve")
-	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
-	prod     = flag.Bool("prod", false, "Serve production assets")
-	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
-	scheme   = flag.String("scheme", "https", "URL scheme")
-
-	tofu *soyhtml.Tofu
+	plugins    = flag.String("plugins", "", "comma seperated plugin paths to serve")
+	port       = flag.String("port", ":8081", "Port to serve HTTP requests on")
+	host       = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
+	scheme     = flag.String("scheme", "https", "URL scheme")
+	cdnPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
 )
 
 func main() {
@@ -61,55 +58,35 @@
 		log.Fatal(err)
 	}
 
-	tofu, err = resolveIndexTemplate()
-	if err != nil {
-		log.Fatal(err)
-	}
-
 	workspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
 	if err := os.Chdir(filepath.Join(workspace, "polygerrit-ui")); err != nil {
 		log.Fatal(err)
 	}
 
-	http.HandleFunc("/index.html", handleIndex)
-
-	if *prod {
-		http.Handle("/", http.FileServer(http.Dir("dist")))
-	} else {
-		http.Handle("/", http.FileServer(http.Dir("app")))
-	}
-
+	http.Handle("/", http.FileServer(http.Dir("app")))
 	http.Handle("/bower_components/",
 		http.FileServer(httpfs.New(zipfs.New(componentsArchive, "bower_components"))))
 	http.Handle("/fonts/",
 		http.FileServer(httpfs.New(zipfs.New(fontsArchive, "fonts"))))
 
-	http.HandleFunc("/changes/", handleRESTProxy)
-	http.HandleFunc("/accounts/", handleRESTProxy)
-	http.HandleFunc("/config/", handleRESTProxy)
-	http.HandleFunc("/projects/", handleRESTProxy)
+	http.HandleFunc("/index.html", handleIndex)
+	http.HandleFunc("/changes/", handleProxy)
+	http.HandleFunc("/accounts/", handleProxy)
+	http.HandleFunc("/config/", handleProxy)
+	http.HandleFunc("/projects/", handleProxy)
 	http.HandleFunc("/accounts/self/detail", handleAccountDetail)
+
 	if len(*plugins) > 0 {
 		http.Handle("/plugins/", http.StripPrefix("/plugins/",
 			http.FileServer(http.Dir("../plugins"))))
 		log.Println("Local plugins from", "../plugins")
 	} else {
-		http.HandleFunc("/plugins/", handleRESTProxy)
+		http.HandleFunc("/plugins/", handleProxy)
 	}
 	log.Println("Serving on port", *port)
 	log.Fatal(http.ListenAndServe(*port, &server{}))
 }
 
-func resolveIndexTemplate() (*soyhtml.Tofu, error) {
-	basePath, err := resourceBasePath()
-	if err != nil {
-		return nil, err
-	}
-	return soy.NewBundle().
-		AddTemplateFile(basePath + ".runfiles/gerrit/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy").
-		CompileToTofu()
-}
-
 func openDataArchive(path string) (*zip.ReadCloser, error) {
 	absBinPath, err := resourceBasePath()
 	if err != nil {
@@ -122,40 +99,40 @@
 	return filepath.Abs(os.Args[0])
 }
 
-func handleIndex(w http.ResponseWriter, r *http.Request) {
-	var obj = map[string]interface{}{
-		"canonicalPath":      "",
-		"staticResourcePath": "",
+func handleIndex(writer http.ResponseWriter, originalRequest *http.Request) {
+	fakeRequest := &http.Request{
+		URL: &url.URL{
+			Path: "/",
+		},
 	}
-	w.Header().Set("Content-Type", "text/html")
-	tofu.Render(w, "com.google.gerrit.httpd.raw.Index", obj)
+	handleProxy(writer, fakeRequest)
 }
 
-func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
-	req := &http.Request{
+func handleProxy(writer http.ResponseWriter, originalRequest *http.Request) {
+	patchedRequest := &http.Request{
 		Method: "GET",
 		URL: &url.URL{
 			Scheme:   *scheme,
-			Host:     *restHost,
-			Opaque:   r.URL.EscapedPath(),
-			RawQuery: r.URL.RawQuery,
+			Host:     *host,
+			Opaque:   originalRequest.URL.EscapedPath(),
+			RawQuery: originalRequest.URL.RawQuery,
 		},
 	}
-	res, err := http.DefaultClient.Do(req)
+	response, err := http.DefaultClient.Do(patchedRequest)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	defer res.Body.Close()
-	for name, values := range res.Header {
+	defer response.Body.Close()
+	for name, values := range response.Header {
 		for _, value := range values {
 			if name != "Content-Length" {
-				w.Header().Add(name, value)
+				writer.Header().Add(name, value)
 			}
 		}
 	}
-	w.WriteHeader(res.StatusCode)
-	if _, err := io.Copy(w, patchResponse(r, res)); err != nil {
+	writer.WriteHeader(response.StatusCode)
+	if _, err := io.Copy(writer, patchResponse(originalRequest, response)); err != nil {
 		log.Println("Error copying response to ResponseWriter:", err)
 		return
 	}
@@ -188,8 +165,10 @@
 	}
 }
 
-func patchResponse(r *http.Request, res *http.Response) io.Reader {
-	switch r.URL.EscapedPath() {
+func patchResponse(req *http.Request, res *http.Response) io.Reader {
+	switch req.URL.EscapedPath() {
+	case "/":
+		return replaceCdn(res.Body)
 	case "/config/server/info":
 		return injectLocalPlugins(res.Body)
 	default:
@@ -197,13 +176,23 @@
 	}
 }
 
-func injectLocalPlugins(r io.Reader) io.Reader {
+func replaceCdn(reader io.Reader) io.Reader {
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(reader)
+	original := buf.String()
+
+	replaced := cdnPattern.ReplaceAllString(original, "")
+
+	return strings.NewReader(replaced)
+}
+
+func injectLocalPlugins(reader io.Reader) io.Reader {
 	if len(*plugins) == 0 {
-		return r
+		return reader
 	}
 	// Skip escape prefix
-	io.CopyN(ioutil.Discard, r, 5)
-	dec := json.NewDecoder(r)
+	io.CopyN(ioutil.Discard, reader, 5)
+	dec := json.NewDecoder(reader)
 
 	var response map[string]interface{}
 	err := dec.Decode(&response)
diff --git a/proto/cache.proto b/proto/cache.proto
index c978069..77b6908 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -75,7 +75,7 @@
 // Instead, we just take the tedious yet simple approach of having a "has_foo"
 // field for each nullable field "foo", indicating whether or not foo is null.
 //
-// Next ID: 19
+// Next ID: 20
 message ChangeNotesStateProto {
   // Effectively required, even though the corresponding ChangeNotesState field
   // is optional, since the field is only absent when NoteDb is disabled, in
@@ -183,6 +183,9 @@
 
   reserved 17;  // read_only_until
   reserved 18;  // has_read_only_until
+
+  // Number of updates to the change's meta ref.
+  int32 update_count = 19;
 }
 
 
@@ -234,3 +237,11 @@
   }
   repeated ExternalIdProto external_id = 1;
 }
+
+// Key for com.google.gerrit.server.git.PureRevertCache.
+// Next ID: 4
+message PureRevertKeyProto {
+  string project = 1;
+  bytes claimed_original = 2;
+  bytes claimed_revert = 3;
+}
diff --git a/proto/testing/BUILD b/proto/testing/BUILD
new file mode 100644
index 0000000..b9032cf
--- /dev/null
+++ b/proto/testing/BUILD
@@ -0,0 +1,12 @@
+proto_library(
+    name = "test_proto",
+    testonly = 1,
+    srcs = ["test.proto"],
+)
+
+java_proto_library(
+    name = "test_java_proto",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    deps = [":test_proto"],
+)
diff --git a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/proto/testing/test.proto
similarity index 63%
copy from java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
copy to proto/testing/test.proto
index a795025..e28c9ff 100644
--- a/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
+++ b/proto/testing/test.proto
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.query.change;
+syntax = "proto2";
 
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import java.util.List;
+package devtools.gerritcodereview.testing;
 
-public interface PluginDefinedAttributesFactory {
-  List<PluginDefinedInfo> create(ChangeData cd);
+option java_package = "com.google.gerrit.proto.testing";
+
+// Test type for ProtobufSerializerTest
+// Next ID: 3
+message SerializableProto {
+  required int32 id = 1;
+  optional string text = 2;
 }
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 78c8684..85f338c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -16,16 +16,15 @@
 
 {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: ?}
+  {@param? polymer2: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -33,6 +32,10 @@
   <meta name="referrer" content="never">{\n}
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
 
+  <noscript>
+    To use PolyGerrit, please enable JavaScript in your browser settings, and then refresh this page.
+  </noscript>
+
   <script>
     window.CLOSURE_NO_DEPS = true;
     {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
@@ -40,6 +43,7 @@
     {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
     {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
     {if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
+    {if $polymer2}window.POLYMER2 = true;{/if}
   </script>{\n}
 
   {if $faviconPath}
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index d3f3666..c32a181 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -263,9 +263,9 @@
 fi
 
 if test -z "$JAVA" ; then
-  echo >&2 "Cannot find a JRE or JDK. Please set JAVA_HOME or"
-  echo >&2 "container.javaHome in $GERRIT_SITE/etc/gerrit.config"
-  echo >&2 "to a >=1.7 JRE"
+  echo >&2 "Cannot find a JRE or JDK. Please ensure that the JAVA_HOME environment"
+  echo >&2 "variable or container.javaHome in $GERRIT_SITE/etc/gerrit.config is"
+  echo >&2 "set to a valid >=1.7 JRE location"
   exit 1
 fi
 
diff --git a/resources/com/google/gerrit/server/change/ChangeMessages.properties b/resources/com/google/gerrit/server/change/ChangeMessages.properties
index 7c1dce3..ec20677 100644
--- a/resources/com/google/gerrit/server/change/ChangeMessages.properties
+++ b/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,9 +1,7 @@
 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/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index 625fd61..d797be3 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -11,6 +11,11 @@
   exit 1
 }
 
+function prereq_modern_git {
+  # "git interpret-trailers --where" was introduced in Git 2.15.0.
+  git interpret-trailers -h 2>&1 | grep -e --where > /dev/null
+}
+
 function test_nonexistent_argument {
   rm -f input
   if ${hook} input ; then
@@ -38,6 +43,38 @@
   fi
 }
 
+function test_keep_cutoff_line {
+  if ! prereq_modern_git ; then
+    echo "old version of Git detected; skipping scissors test."
+    return 0
+  fi
+  rm -f input
+  cat << EOF > input
+Do something nice
+
+# Please enter the commit message for your changes.
+# ------------------------ >8 ------------------------
+# Do not modify or remove the line above.
+# Everything below it will be ignored.
+diff --git a/file.txt b/file.txt
+index 625fd613d9..03aeba3b21 100755
+--- a/file.txt
++++ b/file.txt
+@@ -38,6 +38,7 @@
+ context
+ line
+ 
++hello, world
+ 
+ context
+ line
+EOF
+  ${hook} input || fail "failed hook execution"
+  grep '>8' input || fail "lost cut-off line"
+  sed -n -e '1,/>8/ p' input >top
+  grep '^Change-Id' top || fail "missing Change-Id above cut-off line"
+}
+
 # a Change-Id already set is preserved.
 function test_preserve_changeid {
   cat << EOF > input
@@ -58,6 +95,21 @@
   fi
 }
 
+# Change-Id should not be inserted if gerrit.createChangeId=false
+function test_suppress_changeid {
+  cat << EOF > input
+bla bla
+EOF
+
+  git config gerrit.createChangeId false
+  ${hook} input || fail "failed hook execution"
+  git config --unset gerrit.createChangeId
+  found=$(grep -c '^Change-Id' input || true)
+  if [[ "${found}" != "0" ]]; then
+    fail "got ${found} Change-Ids, want 0"
+  fi
+}
+
 # Change-Id goes after existing trailers.
 function test_at_end {
   cat << EOF > input
diff --git a/resources/com/google/gerrit/server/mail/Abandoned.soy b/resources/com/google/gerrit/server/mail/Abandoned.soy
index 623cfe26..2785ffc 100644
--- a/resources/com/google/gerrit/server/mail/Abandoned.soy
+++ b/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -19,12 +19,12 @@
 /**
  * .Abandoned template will determine the contents of the email related to a
  * change being abandoned.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
  */
 {template .Abandoned kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   {$fromName} has abandoned this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/AbandonedHtml.soy b/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
index 75d940f..9ad996e 100644
--- a/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
@@ -16,12 +16,10 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param coverLetter
- * @param email
- * @param fromName
- */
 {template .AbandonedHtml}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
   <p>
     {$fromName} <strong>abandoned</strong> this change.
   </p>
diff --git a/resources/com/google/gerrit/server/mail/AddKey.soy b/resources/com/google/gerrit/server/mail/AddKey.soy
index be76aee..8b609cf 100644
--- a/resources/com/google/gerrit/server/mail/AddKey.soy
+++ b/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -19,9 +19,9 @@
 /**
  * The .AddKey template will determine the contents of the email related to
  * adding a new SSH or GPG key to an account.
- * @param email
  */
 {template .AddKey kind="text"}
+  {@param email: ?}
   One or more new {$email.keyType} keys have been added to Gerrit Code Review at
   {sp}{$email.gerritHost}:
 
diff --git a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
index 04a0635..ed4f435 100644
--- a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -16,10 +16,8 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param email
- */
 {template .AddKeyHtml}
+  {@param email: ?}
   <p>
     One or more new {$email.keyType} keys have been added to Gerrit Code Review
     at {$email.gerritHost}:
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
index f1d201b..a8170ca 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooter.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -19,9 +19,9 @@
 /**
  * The .ChangeFooter template will determine the contents of the footer text
  * that will be appended to ALL emails related to changes.
- * @param email
  */
 {template .ChangeFooter kind="text"}
+  {@param email: ?}
   --{sp}
   {\n}
 
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index f802366..b619c53 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -16,11 +16,9 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param change
- * @param email
- */
 {template .ChangeFooterHtml}
+  {@param change: ?}
+  {@param email: ?}
   {if $email.changeUrl or $email.settingsUrl}
     <p>
       {if $email.changeUrl}
@@ -38,7 +36,7 @@
   {if $email.changeUrl}
     <div itemscope itemtype="http://schema.org/EmailMessage">
       <div itemscope itemprop="action" itemtype="http://schema.org/ViewAction">
-        <link itemprop="url" href="{$email.changeUrl |blessStringAsTrustedResourceUrlForLegacy}"/>
+        <link itemprop="url" href="{$email.changeUrl}"/>
         <meta itemprop="name" content="View Change"/>
       </div>
     </div>
diff --git a/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
index 48ec9a2..7fcd213 100644
--- a/resources/com/google/gerrit/server/mail/ChangeSubject.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -19,13 +19,13 @@
 /**
  * The .ChangeSubject template will determine the contents of the email subject
  * line for ALL emails related to changes.
- * @param branch
- * @param change
- * @param shortProjectName
- * @param instanceAndProjectName
- * @param addInstanceNameInSubject boolean
  */
 {template .ChangeSubject kind="text"}
+  {@param branch: ?}
+  {@param change: ?}
+  {@param shortProjectName: ?}
+  {@param instanceAndProjectName: ?}
+  {@param addInstanceNameInSubject: ?}  /** boolean */
   {if not $addInstanceNameInSubject}
     Change in {$shortProjectName}[{$branch.shortName}]: {$change.shortSubject}
   {else}
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index f9a11cd..1eb016b 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -19,13 +19,13 @@
 /**
  * The .Comment template will determine the contents of the email related to a
  * user submitting comments on changes.
- * @param change
- * @param coverLetter
- * @param email
- * @param fromName
- * @param commentFiles
  */
 {template .Comment kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param commentFiles: ?}
   {$fromName} has posted comments on this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
diff --git a/resources/com/google/gerrit/server/mail/CommentHtml.soy b/resources/com/google/gerrit/server/mail/CommentHtml.soy
index d554258..534cbdb 100644
--- a/resources/com/google/gerrit/server/mail/CommentHtml.soy
+++ b/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -16,15 +16,13 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param commentFiles
- * @param commentCount
- * @param email
- * @param labels
- * @param patchSet
- * @param patchSetCommentBlocks
- */
 {template .CommentHtml}
+  {@param commentFiles: ?}
+  {@param commentCount: ?}
+  {@param email: ?}
+  {@param labels: ?}
+  {@param patchSet: ?}
+  {@param patchSetCommentBlocks: ?}
   {let $commentHeaderStyle kind="css"}
     margin-bottom: 4px;
   {/let}
diff --git a/resources/com/google/gerrit/server/mail/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/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 77d6f0f..8159ac1 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -209,7 +209,10 @@
 sql = text/x-sql
 ss = text/x-scheme
 st = text/x-stsrc
+star = text/x-python
 stex = text/x-stex
+sv = text/x-systemverilog
+svh = text/x-systemverilog
 swift = text/x-swift
 tcl = text/x-tcl
 tex = text/x-latex
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 53f2995..2901232 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -27,6 +27,11 @@
   exit 1
 fi
 
+# Do not create a change id if requested
+if test "false" = "`git config --bool --get gerrit.createChangeId`" ; then
+  exit 0
+fi
+
 # $RANDOM will be undefined if not using bash, so don't use set -u
 random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
 dest="$1.tmp.${random}"
@@ -43,11 +48,6 @@
   exit 1
 fi
 
-if ! mv "${dest}" "$1" ; then
-  echo "cannot mv ${dest} to $1"
-  exit 1
-fi
-
 # Avoid the --in-place option which only appeared in Git 2.8
 # Avoid the --if-exists option which only appeared in Git 2.15
 if ! git -c trailer.ifexists=doNothing interpret-trailers \
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl
index 97d68d6..825ac98 100644
--- a/tools/bzl/asciidoc.bzl
+++ b/tools/bzl/asciidoc.bzl
@@ -41,17 +41,17 @@
 
 _replace_macros = rule(
     attrs = {
-        "_exe": attr.label(
-            default = Label("//Documentation:replace_macros.py"),
-            allow_single_file = True,
-        ),
         "src": attr.label(
             mandatory = True,
             allow_single_file = [".txt"],
         ),
-        "suffix": attr.string(mandatory = True),
-        "searchbox": attr.bool(default = True),
         "out": attr.output(mandatory = True),
+        "searchbox": attr.bool(default = True),
+        "suffix": attr.string(mandatory = True),
+        "_exe": attr.label(
+            default = Label("//Documentation:replace_macros.py"),
+            allow_single_file = True,
+        ),
     },
     implementation = _replace_macros_impl,
 )
@@ -108,23 +108,23 @@
     )
 
 _asciidoc_attrs = {
+    "srcs": attr.label_list(
+        mandatory = True,
+        allow_files = True,
+    ),
+    "attributes": attr.string_list(),
+    "backend": attr.string(),
+    "suffix": attr.string(mandatory = True),
+    "version": attr.label(
+        default = Label("//:version.txt"),
+        allow_single_file = True,
+    ),
     "_exe": attr.label(
         default = Label("//java/com/google/gerrit/asciidoctor:asciidoc"),
         cfg = "host",
         allow_files = True,
         executable = True,
     ),
-    "srcs": attr.label_list(
-        mandatory = True,
-        allow_files = True,
-    ),
-    "version": attr.label(
-        default = Label("//:version.txt"),
-        allow_single_file = True,
-    ),
-    "suffix": attr.string(mandatory = True),
-    "backend": attr.string(),
-    "attributes": attr.string_list(),
 }
 
 _asciidoc = rule(
@@ -279,11 +279,11 @@
             mandatory = True,
             allow_single_file = [".zip"],
         ),
+        "directory": attr.string(mandatory = True),
         "resources": attr.label_list(
             mandatory = True,
             allow_files = True,
         ),
-        "directory": attr.string(mandatory = True),
     },
     outputs = {
         "out": "%{name}.zip",
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index b60e4e4..0d43be7 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,15 +1,15 @@
 def _classpath_collector(ctx):
-    all = depset()
+    all = []
     for d in ctx.attr.deps:
         if hasattr(d, "java"):
-            all += d.java.transitive_runtime_deps
+            all.append(d.java.transitive_runtime_deps)
             if hasattr(d.java.compilation_info, "runtime_classpath"):
-                all += d.java.compilation_info.runtime_classpath
+                all.append(d.java.compilation_info.runtime_classpath)
         elif hasattr(d, "files"):
-            all += d.files
+            all.append(d.files)
 
-    as_strs = [c.path for c in all]
-    ctx.file_action(
+    as_strs = [c.path for c in depset(transitive = all).to_list()]
+    ctx.actions.write(
         output = ctx.outputs.runtime,
         content = "\n".join(sorted(as_strs)),
     )
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 8f2316c..fcf9f33 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -17,13 +17,10 @@
 def _impl(ctx):
     zip_output = ctx.outputs.zip
 
-    transitive_jar_set = depset()
-    source_jars = depset()
-    for l in ctx.attr.libs:
-        source_jars += l.java.source_jars
-        transitive_jar_set += l.java.transitive_deps
+    transitive_jars = depset(transitive = [l.java.transitive_deps for l in ctx.attr.libs])
+    source_jars = depset(transitive = [l.java.source_jars for l in ctx.attr.libs])
 
-    transitive_jar_paths = [j.path for j in transitive_jar_set]
+    transitive_jar_paths = [j.path for j in transitive_jars.to_list()]
     dir = ctx.outputs.zip.path + ".dir"
     source = ctx.outputs.zip.path + ".source"
     external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs
@@ -32,7 +29,7 @@
         "export TZ",
         "rm -rf %s" % source,
         "mkdir %s" % source,
-        " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]),
+        " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars.to_list()]),
         "rm -rf %s" % dir,
         "mkdir %s" % dir,
         " ".join([
@@ -56,17 +53,17 @@
         "(cd %s && zip -Xqr ../%s *)" % (dir, ctx.outputs.zip.basename),
     ]
     ctx.actions.run_shell(
-        inputs = list(transitive_jar_set) + list(source_jars) + ctx.files._jdk,
+        inputs = transitive_jars.to_list() + source_jars.to_list() + ctx.files._jdk,
         outputs = [zip_output],
         command = " && ".join(cmd),
     )
 
 java_doc = rule(
     attrs = {
+        "external_docs": attr.string_list(),
         "libs": attr.label_list(allow_files = False),
         "pkgs": attr.string_list(),
         "title": attr.string(),
-        "external_docs": attr.string_list(),
         "_jdk": attr.label(
             default = Label("@bazel_tools//tools/jdk:current_java_runtime"),
             allow_files = True,
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 23d1ccd..83c13a3 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -1,10 +1,10 @@
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
+load("//lib/js:npm.bzl", "NPM_SHA1S", "NPM_VERSIONS")
+
 NPMJS = "NPMJS"
 
 GERRIT = "GERRIT:"
 
-load("//lib/js:npm.bzl", "NPM_SHA1S", "NPM_VERSIONS")
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
-
 def _npm_tarball(name):
     return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
 
@@ -20,9 +20,9 @@
     dest = ctx.path(base)
     repository = ctx.attr.repository
     if repository == GERRIT:
-        url = "http://gerrit-maven.storage.googleapis.com/npm-packages/%s" % filename
+        url = "https://gerrit-maven.storage.googleapis.com/npm-packages/%s" % filename
     elif repository == NPMJS:
-        url = "http://registry.npmjs.org/%s/-/%s" % (name, filename)
+        url = "https://registry.npmjs.org/%s/-/%s" % (name, filename)
     else:
         fail("repository %s not in {%s,%s}" % (repository, GERRIT, NPMJS))
 
@@ -37,9 +37,9 @@
 
 npm_binary = repository_rule(
     attrs = {
+        "repository": attr.string(default = NPMJS),
         # Label resolves within repo of the .bzl file.
         "_download_script": attr.label(default = Label("//tools:download_file.py")),
-        "repository": attr.string(default = NPMJS),
     },
     local = True,
     implementation = _npm_binary_impl,
@@ -120,31 +120,31 @@
 bower_archive = repository_rule(
     _bower_archive,
     attrs = {
-        "_bower_archive": attr.label(default = Label("@bower//:%s" % _npm_tarball("bower"))),
-        "_run_npm": attr.label(default = Label("//tools/js:run_npm_binary.py")),
-        "_download_bower": attr.label(default = Label("//tools/js:download_bower.py")),
-        "sha1": attr.string(mandatory = True),
-        "version": attr.string(mandatory = True),
         "package": attr.string(mandatory = True),
         "semver": attr.string(),
+        "sha1": attr.string(mandatory = True),
+        "version": attr.string(mandatory = True),
+        "_bower_archive": attr.label(default = Label("@bower//:%s" % _npm_tarball("bower"))),
+        "_download_bower": attr.label(default = Label("//tools/js:download_bower.py")),
+        "_run_npm": attr.label(default = Label("//tools/js:run_npm_binary.py")),
     },
 )
 
 def _bower_component_impl(ctx):
-    transitive_zipfiles = depset([ctx.file.zipfile])
-    for d in ctx.attr.deps:
-        transitive_zipfiles += d.transitive_zipfiles
+    transitive_zipfiles = depset(
+        direct = [ctx.file.zipfile],
+        transitive = [d.transitive_zipfiles for d in ctx.attr.deps],
+    )
 
-    transitive_licenses = depset()
-    if ctx.file.license:
-        transitive_licenses += depset([ctx.file.license])
+    transitive_licenses = depset(
+        direct = [ctx.file.license],
+        transitive = [d.transitive_licenses for d in ctx.attr.deps],
+    )
 
-    for d in ctx.attr.deps:
-        transitive_licenses += d.transitive_licenses
-
-    transitive_versions = depset(ctx.files.version_json)
-    for d in ctx.attr.deps:
-        transitive_versions += d.transitive_versions
+    transitive_versions = depset(
+        direct = ctx.files.version_json,
+        transitive = [d.transitive_versions for d in ctx.attr.deps],
+    )
 
     return struct(
         transitive_licenses = transitive_licenses,
@@ -183,12 +183,12 @@
         mnemonic = "GenBowerZip",
     )
 
-    licenses = depset()
+    licenses = []
     if ctx.file.license:
-        licenses += depset([ctx.file.license])
+        licenses.append(ctx.file.license)
 
     return struct(
-        transitive_licenses = licenses,
+        transitive_licenses = depset(licenses),
         transitive_versions = depset(),
         transitive_zipfiles = list([ctx.outputs.zip]),
     )
@@ -207,12 +207,12 @@
 _bower_component = rule(
     _bower_component_impl,
     attrs = dict(_common_attrs.items() + {
-        "zipfile": attr.label(allow_single_file = [".zip"]),
         "license": attr.label(allow_single_file = True),
-        "version_json": attr.label(allow_files = [".json"]),
 
         # If set, define by hand, and don't regenerate this entry in bower2bazel.
         "seed": attr.bool(default = False),
+        "version_json": attr.label(allow_files = [".json"]),
+        "zipfile": attr.label(allow_single_file = [".zip"]),
     }.items()),
 )
 
@@ -233,21 +233,22 @@
     """A bunch of bower components zipped up."""
     zips = depset()
     for d in ctx.attr.deps:
-        zips += d.transitive_zipfiles
+        files = d.transitive_zipfiles
 
-    versions = depset()
-    for d in ctx.attr.deps:
-        versions += d.transitive_versions
+        # TODO(davido): Make sure the field always contains a depset
+        if type(files) == "list":
+            files = depset(files)
+        zips = depset(transitive = [zips, files])
 
-    licenses = depset()
-    for d in ctx.attr.deps:
-        licenses += d.transitive_versions
+    versions = depset(transitive = [d.transitive_versions for d in ctx.attr.deps])
+
+    licenses = depset(transitive = [d.transitive_versions for d in ctx.attr.deps])
 
     out_zip = ctx.outputs.zip
     out_versions = ctx.outputs.version_json
 
     ctx.actions.run_shell(
-        inputs = list(zips),
+        inputs = zips.to_list(),
         outputs = [out_zip],
         command = " && ".join([
             "p=$PWD",
@@ -256,7 +257,7 @@
             "rm -rf %s.dir" % out_zip.path,
             "mkdir -p %s.dir/bower_components" % out_zip.path,
             "cd %s.dir/bower_components" % out_zip.path,
-            "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
+            "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips.to_list()])),
             "cd ..",
             "find . -exec touch -t 198001010000 '{}' ';'",
             "zip -Xqr $p/%s bower_components/*" % out_zip.path,
@@ -265,10 +266,10 @@
     )
 
     ctx.actions.run_shell(
-        inputs = list(versions),
+        inputs = versions.to_list(),
         outputs = [out_versions],
         mnemonic = "BowerVersions",
-        command = "(echo '{' ; for j in  %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path),
+        command = "(echo '{' ; for j in  %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions.to_list()]), out_versions.path),
     )
 
     return struct(
@@ -281,8 +282,8 @@
     _bower_component_bundle_impl,
     attrs = _common_attrs,
     outputs = {
-        "zip": "%{name}.zip",
         "version_json": "%{name}-versions.json",
+        "zip": "%{name}.zip",
     },
 )
 
@@ -299,11 +300,7 @@
 
     # intermediate artifact if split is wanted.
     if ctx.attr.split:
-        bundled = ctx.new_file(
-            ctx.configuration.genfiles_dir,
-            ctx.outputs.html,
-            ".bundled.html",
-        )
+        bundled = ctx.actions.declare_file(ctx.outputs.html.path + ".bundled.html")
     else:
         bundled = ctx.outputs.html
     destdir = ctx.outputs.html.path + ".dir"
@@ -394,11 +391,6 @@
 _bundle_rule = rule(
     _bundle_impl,
     attrs = {
-        "deps": attr.label_list(providers = ["transitive_zipfiles"]),
-        "app": attr.label(
-            mandatory = True,
-            allow_single_file = True,
-        ),
         "srcs": attr.label_list(allow_files = [
             ".js",
             ".html",
@@ -406,12 +398,13 @@
             ".css",
             ".ico",
         ]),
-        "pkg": attr.string(mandatory = True),
-        "split": attr.bool(default = True),
-        "_run_npm": attr.label(
-            default = Label("//tools/js:run_npm_binary.py"),
+        "app": attr.label(
+            mandatory = True,
             allow_single_file = True,
         ),
+        "pkg": attr.string(mandatory = True),
+        "split": attr.bool(default = True),
+        "deps": attr.label_list(providers = ["transitive_zipfiles"]),
         "_bundler_archive": attr.label(
             default = Label("@polymer-bundler//:%s" % _npm_tarball("polymer-bundler")),
             allow_single_file = True,
@@ -420,15 +413,19 @@
             default = Label("@crisper//:%s" % _npm_tarball("crisper")),
             allow_single_file = True,
         ),
+        "_run_npm": attr.label(
+            default = Label("//tools/js:run_npm_binary.py"),
+            allow_single_file = True,
+        ),
     },
     outputs = _bundle_output_func,
 )
 
 def bundle_assets(*args, **kwargs):
     """Combine html, js, css files and optionally split into js and html bundles."""
-    _bundle_rule(*args, pkg = native.package_name(), **kwargs)
+    _bundle_rule(pkg = native.package_name(), *args, **kwargs)
 
-def polygerrit_plugin(name, app, srcs = [], assets = None, plugin_name = None, **kwargs):
+def polygerrit_plugin(name, app, srcs = [], deps = [], externs = [], assets = None, plugin_name = None, **kwargs):
     """Bundles plugin dependencies for deployment.
 
     This rule bundles all Polymer elements and JS dependencies into .html and .js files.
@@ -438,6 +435,7 @@
     Args:
       name: String, rule name.
       app: String, the main or root source file.
+      externs: Fileset, external definitions that should not be bundled.
       assets: Fileset, additional files to be used by plugin in runtime, exported to "plugins/${name}/static".
       plugin_name: String, plugin name. ${name} is used if not provided.
     """
@@ -453,6 +451,7 @@
             name = name + "_combined",
             app = app,
             srcs = srcs,
+            deps = deps,
             pkg = native.package_name(),
             **kwargs
         )
@@ -462,7 +461,7 @@
 
     closure_js_library(
         name = name + "_closure_lib",
-        srcs = js_srcs,
+        srcs = js_srcs + externs,
         convention = "GOOGLE",
         no_closure_library = True,
         deps = [
@@ -473,7 +472,7 @@
 
     closure_js_binary(
         name = name + "_bin",
-        compilation_level = "SIMPLE",
+        compilation_level = "WHITESPACE_ONLY",
         defs = [
             "--polymer_version=1",
             "--language_out=ECMASCRIPT6",
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index ccde467..5da5f05 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -50,7 +50,7 @@
     classes = ",".join(
         [_AsClassName(x) for x in ctx.attr.srcs],
     )
-    ctx.file_action(output = ctx.outputs.out, content = _OUTPUT % (
+    ctx.actions.write(output = ctx.outputs.out, content = _OUTPUT % (
         classes,
         ctx.attr.outname,
     ))
@@ -72,7 +72,7 @@
 ]
 
 def junit_tests(name, srcs, **kwargs):
-    s_name = name + "TestSuite"
+    s_name = name.replace("-", "_") + "TestSuite"
     _GenSuite(
         name = s_name,
         srcs = srcs,
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index ebe57f2..2779130 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -54,6 +54,8 @@
     # We don't want any blank line before "= Gerrit Code Review - Licenses"
     print("""= Gerrit Code Review - Licenses
 
+// DO NOT EDIT - GENERATED AUTOMATICALLY.
+
 Gerrit open source software is licensed under the <<Apache2_0,Apache
 License 2.0>>.  Executable distributions also include other software
 components that are provided under additional licenses.
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index d059216..5a6bf7f 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -26,7 +26,7 @@
     native.genrule(
         name = "gen_license_txt_" + name,
         cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
-        outs = [name + ".txt"],
+        outs = [name + ".gen.txt"],
         tools = tools,
         **kwargs
     )
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 7fb042f..7bc07b1 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -186,15 +186,15 @@
 maven_jar = repository_rule(
     attrs = {
         "artifact": attr.string(mandatory = True),
+        "attach_source": attr.bool(default = True),
+        "exclude": attr.string_list(),
+        "repository": attr.string(default = MAVEN_CENTRAL),
         "sha1": attr.string(),
         "src_sha1": attr.string(),
-        "_download_script": attr.label(default = Label("//tools:download_file.py")),
-        "repository": attr.string(default = MAVEN_CENTRAL),
-        "attach_source": attr.bool(default = True),
         "unsign": attr.bool(default = False),
-        "deps": attr.string_list(),
         "exports": attr.string_list(),
-        "exclude": attr.string_list(),
+        "deps": attr.string_list(),
+        "_download_script": attr.label(default = Label("//tools:download_file.py")),
     },
     local = True,
     implementation = _maven_jar_impl,
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 1fd1c81..a480908 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -75,36 +75,40 @@
     ]
 
     # Add lib
-    transitive_lib_deps = depset()
+    transitive_libs = []
     for l in ctx.attr.libs:
         if hasattr(l, "java"):
-            transitive_lib_deps += l.java.transitive_runtime_deps
+            transitive_libs.append(l.java.transitive_runtime_deps)
         elif hasattr(l, "files"):
-            transitive_lib_deps += l.files
+            transitive_libs.append(l.files)
 
-    for dep in transitive_lib_deps:
+    transitive_lib_deps = depset(transitive = transitive_libs)
+    for dep in transitive_lib_deps.to_list():
         cmd += _add_file(dep, build_output + "/WEB-INF/lib/")
         inputs.append(dep)
 
     # Add pgm lib
-    transitive_pgmlib_deps = depset()
+    transitive_pgmlibs = []
     for l in ctx.attr.pgmlibs:
-        transitive_pgmlib_deps += l.java.transitive_runtime_deps
+        transitive_pgmlibs.append(l.java.transitive_runtime_deps)
 
-    for dep in transitive_pgmlib_deps:
+    transitive_pgmlib_deps = depset(transitive = transitive_pgmlibs)
+    for dep in transitive_pgmlib_deps.to_list():
         if dep not in inputs:
             cmd += _add_file(dep, build_output + "/WEB-INF/pgm-lib/")
             inputs.append(dep)
 
     # Add context
-    transitive_context_deps = depset()
+    transitive_context_libs = []
     if ctx.attr.context:
         for jar in ctx.attr.context:
             if hasattr(jar, "java"):
-                transitive_context_deps += jar.java.transitive_runtime_deps
+                transitive_context_libs.append(jar.java.transitive_runtime_deps)
             elif hasattr(jar, "files"):
-                transitive_context_deps += jar.files
-    for dep in transitive_context_deps:
+                transitive_context_libs.append(jar.files)
+
+    transitive_context_deps = depset(transitive = transitive_context_libs)
+    for dep in transitive_context_deps.to_list():
         cmd += _add_context(dep, build_output)
         inputs.append(dep)
 
diff --git a/tools/bzl/plugins.bzl b/tools/bzl/plugins.bzl
index 4b4065f..fdb4c1d 100644
--- a/tools/bzl/plugins.bzl
+++ b/tools/bzl/plugins.bzl
@@ -3,6 +3,7 @@
     "commit-message-length-validator",
     "delete-project",
     "download-commands",
+    "gitiles",
     "hooks",
     "replication",
     "reviewnotes",
diff --git a/tools/coverage.sh b/tools/coverage.sh
index 72e1d5b..11e50e6 100755
--- a/tools/coverage.sh
+++ b/tools/coverage.sh
@@ -22,15 +22,27 @@
 
 # coverage is expensive to run; use --jobs=2 to avoid overloading the
 # machine.
-bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ... -//javatests/com/google/gerrit/common:auto_value_tests
+bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ...
 
 # The coverage data contains filenames relative to the Java root, and
 # genhtml has no logic to search these elsewhere. Workaround this
 # limitation by running genhtml in a directory with the files in the
 # right place. Also -inexplicably- genhtml wants to have the source
 # files relative to the output directory.
-mkdir -p ${destdir}/
-cp -a */src/{main,test}/java/* ${destdir}/
+mkdir -p ${destdir}/java
+cp -r {java,javatests}/* ${destdir}/java
+
+mkdir -p ${destdir}/plugins
+for plugin in `find plugins/ -type d` -maxdepth 1
+do
+  mkdir -p ${destdir}/${plugin}/java
+  cp -r plugins/*/{java,javatests}/* ${destdir}/${plugin}/java
+
+  # for backwards compatibility support plugins with old file structure
+  mkdir -p ${destdir}/${plugin}/src/{main,test}/java
+  cp -r plugins/*/src/main/java/* ${destdir}/${plugin}/src/main/java
+  cp -r plugins/*/src/test/java/* ${destdir}/${plugin}/src/test/java
+done
 
 base=$(bazel info bazel-testlogs)
 for f in $(find ${base}  -name 'coverage.dat') ; do
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index f6016cd..5ef3a46 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -1,5 +1,5 @@
-load("//tools/bzl:pkg_war.bzl", "LIBS", "PGMLIBS")
 load("//tools/bzl:classpath.bzl", "classpath_collector")
+load("//tools/bzl:pkg_war.bzl", "LIBS", "PGMLIBS")
 load(
     "//tools/bzl:plugins.bzl",
     "CORE_PLUGINS",
@@ -12,6 +12,10 @@
     "//javatests/com/google/gerrit/server:server_tests",
 ]
 
+TEST_DEPS_GENERATED = [
+    "//proto/testing:test_java_proto",
+]
+
 DEPS = [
     "//java/com/google/gerrit/acceptance:lib",
     "//java/com/google/gerrit/server",
@@ -26,13 +30,13 @@
 java_library(
     name = "classpath",
     testonly = True,
-    runtime_deps = LIBS + PGMLIBS + DEPS,
+    runtime_deps = LIBS + PGMLIBS + DEPS + TEST_DEPS_GENERATED,
 )
 
 classpath_collector(
     name = "main_classpath_collect",
     testonly = True,
-    deps = LIBS + PGMLIBS + DEPS + TEST_DEPS +
+    deps = LIBS + PGMLIBS + DEPS + TEST_DEPS + TEST_DEPS_GENERATED +
            ["//plugins/%s:%s__plugin" % (n, n) for n in CORE_PLUGINS + CUSTOM_PLUGINS] +
            ["//plugins/%s:%s__plugin_test_deps" % (n, n) for n in CUSTOM_PLUGINS_TEST_DEPS],
 )
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index e9e249f..46aeb31 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -222,7 +222,10 @@
 
         p = path.join(s, 'java')
         if path.exists(p):
-            classpathentry('src', p, out=out)
+            classpathentry('src', p, out=out + '/main')
+            p = path.join(s, 'javatests')
+            if path.exists(p):
+                classpathentry('src', p, out=out + '/test')
             continue
 
         for env in ['main', 'test']:
@@ -254,6 +257,7 @@
 
     for p in sorted(proto):
         s = p.replace('-fastbuild/bin/proto/lib', '-fastbuild/genfiles/proto/')
+        s = p.replace('-fastbuild/bin/proto/testing/lib', '-fastbuild/genfiles/proto/testing/')
         s = s.replace('.jar', '-src.jar')
         classpathentry('lib', p, s)
 
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 7b24524..e728cc3 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -204,11 +204,12 @@
     for d in data:
         if d["name"] in seeds:
             continue
-        out.write("""  bower_archive(
-    name = "%(name)s",
-    package = "%(normalized-name)s",
-    version = "%(version)s",
-    sha1 = "%(bazel-sha1)s")
+        out.write("""    bower_archive(
+        name = "%(name)s",
+        package = "%(normalized-name)s",
+        version = "%(version)s",
+        sha1 = "%(bazel-sha1)s",
+    )
 """ % d)
 
 
@@ -216,21 +217,21 @@
     out.write('load("//tools/bzl:js.bzl", "bower_component")\n\n')
     out.write('def define_bower_components():\n')
     for d in data:
-        out.write("  bower_component(\n")
-        out.write("    name = \"%s\",\n" % d["name"])
-        out.write("    license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"])
+        out.write("    bower_component(\n")
+        out.write("        name = \"%s\",\n" % d["name"])
+        out.write("        license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"])
         deps = sorted(d.get("dependencies", {}).keys())
         if deps:
             if len(deps) == 1:
-                out.write("    deps = [ \":%s\" ],\n" % deps[0])
+                out.write("        deps = [\":%s\"],\n" % deps[0])
             else:
-                out.write("    deps = [\n")
+                out.write("        deps = [\n")
                 for dep in deps:
-                    out.write("      \":%s\",\n" % dep)
-                out.write("    ],\n")
+                    out.write("            \":%s\",\n" % dep)
+                out.write("        ],\n")
         if d["name"] in seeds:
-            out.write("    seed = True,\n")
-        out.write("  )\n")
+            out.write("        seed = True,\n")
+        out.write("    )\n")
     # done
 
 
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 5625e9e..14a07d1 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.0-SNAPSHOT</version>
+  <version>3.1.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
@@ -26,10 +26,7 @@
       <name>Alice Kober-Sotzek</name>
     </developer>
     <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
+      <name>Ben Rohlfs</name>
     </developer>
     <developer>
       <name>Dave Borowitz</name>
@@ -50,12 +47,6 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -65,17 +56,14 @@
       <name>Martin Fick</name>
     </developer>
     <developer>
+      <name>Ole Rehmsen</name>
+    </developer>
+    <developer>
+      <name>Patrick Hiesel</name>
+    </developer>
+    <developer>
       <name>Saša Živkov</name>
     </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
   </developers>
 
   <mailingLists>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 21719ee..0cf1448 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.0-SNAPSHOT</version>
+  <version>3.1.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
@@ -26,10 +26,7 @@
       <name>Alice Kober-Sotzek</name>
     </developer>
     <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
+      <name>Ben Rohlfs</name>
     </developer>
     <developer>
       <name>Dave Borowitz</name>
@@ -50,12 +47,6 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -65,20 +56,14 @@
       <name>Martin Fick</name>
     </developer>
     <developer>
+      <name>Ole Rehmsen</name>
+    </developer>
+    <developer>
       <name>Patrick Hiesel</name>
     </developer>
     <developer>
       <name>Saša Živkov</name>
     </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
   </developers>
 
   <mailingLists>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index dfa0eef..ebb66b4 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.0-SNAPSHOT</version>
+  <version>3.1.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
@@ -26,10 +26,7 @@
       <name>Alice Kober-Sotzek</name>
     </developer>
     <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
+      <name>Ben Rohlfs</name>
     </developer>
     <developer>
       <name>Dave Borowitz</name>
@@ -50,12 +47,6 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -65,17 +56,14 @@
       <name>Martin Fick</name>
     </developer>
     <developer>
+      <name>Ole Rehmsen</name>
+    </developer>
+    <developer>
+      <name>Patrick Hiesel</name>
+    </developer>
+    <developer>
       <name>Saša Živkov</name>
     </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
   </developers>
 
   <mailingLists>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 9ac81c7..51e517b 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.0-SNAPSHOT</version>
+  <version>3.1.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
@@ -26,10 +26,7 @@
       <name>Alice Kober-Sotzek</name>
     </developer>
     <developer>
-      <name>Andrew Bonventre</name>
-    </developer>
-    <developer>
-      <name>Becky Siegel</name>
+      <name>Ben Rohlfs</name>
     </developer>
     <developer>
       <name>Dave Borowitz</name>
@@ -50,12 +47,6 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
-      <name>Kasper Nilsson</name>
-    </developer>
-    <developer>
-      <name>Logan Hanks</name>
-    </developer>
-    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
@@ -65,17 +56,14 @@
       <name>Martin Fick</name>
     </developer>
     <developer>
+      <name>Ole Rehmsen</name>
+    </developer>
+    <developer>
+      <name>Patrick Hiesel</name>
+    </developer>
+    <developer>
       <name>Saša Živkov</name>
     </developer>
-    <developer>
-      <name>Shawn Pearce</name>
-    </developer>
-    <developer>
-      <name>Viktar Donich</name>
-    </developer>
-    <developer>
-      <name>Wyatt Allen</name>
-    </developer>
   </developers>
 
   <mailingLists>
diff --git a/tools/setup_gjf.sh b/tools/setup_gjf.sh
index de2e0cc..119f9af 100755
--- a/tools/setup_gjf.sh
+++ b/tools/setup_gjf.sh
@@ -17,7 +17,7 @@
 set -eu
 
 # Keep this version in sync with dev-contributing.txt.
-VERSION=${1:-1.6}
+VERSION=${1:-1.7}
 
 case "$VERSION" in
 1.3)
@@ -29,6 +29,9 @@
 1.6)
     SHA1="02b3e84e52d2473e2c4868189709905a51647d03"
     ;;
+1.7)
+    SHA1="b6d34a51e579b08db7c624505bdf9af4397f1702"
+    ;;
 *)
     echo "unknown google-java-format version: $VERSION"
     exit 1
diff --git a/tools/util.py b/tools/util.py
index 3817f75..172ecfe 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -15,14 +15,12 @@
 from os import path
 
 REPO_ROOTS = {
-    'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
-    'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
-    'GERRIT_API':
-        'https://gerrit-api.commondatastorage.googleapis.com/release',
-    'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
-    'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
-    'MAVEN_SNAPSHOT':
-        'https://oss.sonatype.org/content/repositories/snapshots',
+  'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
+  'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
+  'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
+  'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
+  'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
+  'MAVEN_SNAPSHOT': 'https://oss.sonatype.org/content/repositories/snapshots',
 }
 
 
diff --git a/version.bzl b/version.bzl
index 20fd8a7..42a576c 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.0-SNAPSHOT"
+GERRIT_VERSION = "3.1.0-SNAPSHOT"