Merge branch 'stable-3.2' into stable-3.3

* stable-3.2:
  Set PerThreadCache as readonly after creating a new patch-set
  Set PerThreadCache as readonly when formatting change e-mails
  Set PerThreadCache as readonly when formatting change JSON
  Set PerThreadCache as readonly after deleting a change
  Set PerThreadCache as readonly after abandoning a change
  Set PerThreadCache as readonly after merging a change
  Set PerThreadCache as readonly after posting review comments
  Introduce unloaders on PerThreadCache entries
  RepoRefCache: Hold a reference to the refDatabase with ref counting
  Remove use of RefCache in ChangeNotes
  Revert "Cache change /meta ref SHA1 for each REST API request"
  Cache change /meta ref SHA1 for each change indexing task

Release-Notes: skip
Change-Id: Ic3981c1fad0a1f8c232a72d858b8521d2407c0ff
diff --git a/.bazelrc b/.bazelrc
index e560f2397..72138d2 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,8 +1,8 @@
-build --workspace_status_command="python3 ./tools/workspace_status.py" --strategy=Closure=worker
+build --workspace_status_command="python3 ./tools/workspace_status.py"
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
-build --java_toolchain //tools:error_prone_warnings_toolchain
+build --java_toolchain=//tools:error_prone_warnings_toolchain
 
 # Enable strict_action_env flag to. For more information on this feature see
 # https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
@@ -13,5 +13,6 @@
 
 test --build_tests_only
 test --test_output=errors
+test --java_toolchain=//tools:error_prone_warnings_toolchain
 
 import %workspace%/tools/remote-bazelrc
diff --git a/.bazelversion b/.bazelversion
index 6aba2b2..af8c8ec 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.2.0
+4.2.2
diff --git a/.gitignore b/.gitignore
index 1e91c8f..00a6217 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
 *.swp
 *~
 .DS_Store
+js-to-ts.sh
 /.apt_generated
 /.apt_generated_tests
 /.bazel_path
@@ -19,7 +20,6 @@
 /.settings/org.eclipse.ltk.core.refactoring.prefs
 /.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
-/.ts-out
 /.vscode
 /bazel-*
 /bin/
@@ -32,6 +32,8 @@
 /node_modules/
 /package-lock.json
 /plugins/*
+!/plugins/package.json
+!/plugins/yarn.lock
 !/plugins/BUILD
 !/plugins/codemirror-editor
 !/plugins/commit-message-length-validator
@@ -50,3 +52,5 @@
 /tools/maven/gerrit-*_pom.xml.asc
 /tools/node_tools
 /tools/polygerrit-updater
+/.ts-out/*
+!/.ts-out/README.md
diff --git a/.ts-out/README.md b/.ts-out/README.md
new file mode 100644
index 0000000..dada30d
--- /dev/null
+++ b/.ts-out/README.md
@@ -0,0 +1,4 @@
+This directory contains compiled js code. Typescript uses subdirectories
+as output directories when runs under IDE.
+
+Bazel doesn't use this directory
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 71c9330..e2d3c6a 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -100,7 +100,7 @@
 Gerrit comes with two predefined groups:
 
 * Administrators
-* Non-Interactive Users
+* Service Users
 
 
 [[administrators]]
@@ -115,9 +115,10 @@
 Gerrit administrators, despite the group name. The group may also be
 renamed.
 
+anchor:non-interactive_users[]
 
-[[non-interactive_users]]
-=== Non-Interactive Users
+[[service_users]]
+=== Service Users
 
 This is the Gerrit "batch" identity. The capabilities
 link:access-control.html#capability_priority['Priority BATCH'] and
@@ -131,10 +132,11 @@
 order to prevent it from grabbing threads from the interactive users.
 
 These users live in a second thread pool, which separates operations
-made by the non-interactive users from the ones made by the interactive
+made by the service users from the ones made by the interactive
 users. This ensures that the interactive users can keep working when
 resources are tight.
 
+Before Gerrit 3.3, the 'Service Users' group was named 'Non-Interactive Users'.
 
 == Account Groups
 
@@ -186,6 +188,10 @@
 project "`All-Projects`".  This inheritance can be configured
 through link:cmd-set-project-parent.html[gerrit set-project-parent].
 
+When projects are set as parent projects, the child projects inherit
+all of the parent's access rights. "`All-Projects`" is treated as a
+parent of all projects.
+
 Per-project access control lists are also supported.
 
 Users are permitted to use the maximum range granted to any of their
@@ -865,6 +871,11 @@
 private changes (even without having the `View Private Changes` access
 right assigned).
 
+**NOTE**: If link:config-gerrit.html#auth.skipFullRefEvaluationIfAllRefsAreVisible[
+auth.skipFullRefEvaluationIfAllRefsAreVisible] is `true` (which is the case by
+default) privates changes and all change edit refs are also visible to users
+that have read access on `refs/*`.
+
 [[category_toggle_work_in_progress_state]]
 === Toggle Work In Progress state
 
@@ -1319,7 +1330,7 @@
 This capability allows the granted group members to create non-interactive
 service accounts.  These service accounts are generally used for automation
 and made to be members of the
-link:access-control.html#non-interactive_users['Non-Interactive users'] group.
+link:access-control.html#service_users['Service users'] group.
 
 
 [[capability_createGroup]]
@@ -1398,7 +1409,7 @@
 
 This capability allows users to use
 link:config-gerrit.html#sshd.batchThreads[the thread pool reserved] for
-link:access-control.html#non-interactive_users['Non-Interactive Users'].
+link:access-control.html#service_users['Service Users'].
 It's a binary value in that granted users either have access to the thread
 pool, or they don't.
 
@@ -1410,7 +1421,7 @@
 default the user is then in the 'INTERACTIVE' thread pool.
 
 'BATCH'::
-If there's a thread pool configured for 'Non-Interactive Users' and a user is
+If there's a thread pool configured for 'Service Users' and a user is
 granted the priority capability with the 'BATCH' mode selected, the user ends
 up in the separate batch user thread pool. This is true unless the user is
 also granted the below 'INTERACTIVE' option.
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 617191f..502b7a7 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -23,7 +23,7 @@
 or event monitoring over link:cmd-stream-events.html[gerrit stream-events].
 
 Note, however, that in this case the account is not implicitly added
-to the 'Non-Interactive Users' group.  The account must be explicitly
+to the 'Service Users' group.  The account must be explicitly
 added to the group with the `--group` option.
 
 If LDAP authentication is being used, the user account is created
@@ -66,10 +66,10 @@
 
 == EXAMPLES
 Create a new batch/role access user account called `watcher` in
-the 'Non-Interactive Users' group.
+the 'Service Users' group.
 
 ----
-$ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --group "'Non-Interactive Users'" --ssh-key - watcher
+$ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --group "'Service Users'" --ssh-key - watcher
 ----
 
 GERRIT
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index 2b6d7af..e547822 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -56,6 +56,26 @@
 The `Change-Id` will not be added if `gerrit.createChangeId` is set
 to `false` in the git config.
 
+If `gerrit.reviewUrl` is set to the base URL of the Gerrit server that
+changes are uploaded to (e.g. `https://gerrit-review.googlesource.com/`)
+in the git config, then instead of adding a `Change-Id` trailer, a `Link`
+trailer will be inserted that will look like this:
+
+----
+Improve foo widget by attaching a bar.
+
+We want a bar, because it improves the foo by providing more
+wizbangery to the dowhatimeanery.
+
+Link: https://gerrit-review.googlesource.com/id/Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b
+Signed-off-by: A. U. Thor <author@example.com>
+----
+
+This link will become a valid link to the review page once the change is
+uploaded to the Gerrit server. Newer versions of the Gerrit server will read
+the change identifier out of the appropriate `Link` trailer and treat it in
+the same way as the change identifier in a `Change-Id` trailer.
+
 == OBTAINING
 
 To obtain the `commit-msg` script use `scp`, `wget` or `curl` to download
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 8a4845c..0a06c28 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -11,7 +11,7 @@
   [--user <NAME> | -u <NAME>]
   [--owned]
   [--visible-to-all]
-  [-q <GROUP>]
+  [-g <GROUP>]
   [--verbose | -v]
 --
 
@@ -67,8 +67,8 @@
 	(groups that are explicitly marked as visible to all registered
 	users).
 
--q::
-	Group that should be inspected. The `-q` option can be specified
+-g::
+	Group that should be inspected. The `-g` option can be specified
 	multiple times to define several groups to be inspected. If
 	specified the listed groups will only contain groups that were
 	specified to be inspected. This is e.g. useful in combination with
diff --git a/Documentation/concept-changes.txt b/Documentation/concept-changes.txt
index 1d275b4..6de787c 100644
--- a/Documentation/concept-changes.txt
+++ b/Documentation/concept-changes.txt
@@ -14,7 +14,7 @@
 
 * Current and previous patch sets
 * <<Change properties>>, such as owner, project, and target branch
-* link:CONCEPT-comments.html[Comments]
+* Comments
 * Votes on link:config-labels.html[Review Labels]
 * The <<change-id>>
 
@@ -203,6 +203,34 @@
 method uses git's link:cmd-hook-commit-msg.html[commit-msg hook]
 to automatically add the Change-Id to each new commit.
 
+== The Link footer
+
+Gerrit also supports the Link footer as an alternative to the Change-Id
+footer. A Link footer looks like this:
+
+....
+    Link: https://gerrit-review.googlesource.com/id/Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b
+....
+
+The advantage of this style of footer is that it usually acts
+as a link directly to the change's review page, provided that
+the change has been uploaded to Gerrit. Projects such as the
+link:https://www.kernel.org/doc/html/latest/maintainer/configure-git.html#creating-commit-links-to-lore-kernel-org[Linux kernel]
+have a convention of adding links to commit messages using the
+Link footer.
+
+If multiple changes have been uploaded to Gerrit with the same
+change ID, for example if a change has been cherry-picked to multiple
+branches, the link will take the user to a list of changes.
+
+The base URL in the footer is required to match the server's base URL.
+If the URL does not match, the server will not recognize the footer
+as a change ID footer.
+
+The link:cmd-hook-commit-msg.html[commit-msg hook] can be configured
+to insert Link footers instead of Change-Id footers by setting the
+property `gerrit.reviewUrl` to the base URL of the Gerrit server.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index b964040..085a5d5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -256,8 +256,8 @@
 if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
 the password in the request is first checked against the HTTP password and, if
 it does not match, it is then validated against the LDAP password.
-Service users that only exist in the Gerrit database are authenticated by their
-HTTP passwords.
+Service users that are link:cmd-create-account.html[internal-only] are
+authenticated by their HTTP passwords.
 
 * `LDAP_BIND`
 +
@@ -611,7 +611,7 @@
 single call would trigger a full LDAP authentication and groups resolution
 which could introduce a noticeable latency on the overall execution
 and produce unwanted load to the LDAP server.
-+
+
 [[auth.gitOAuthProvider]]auth.gitOAuthProvider::
 +
 Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
@@ -677,8 +677,10 @@
 [[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus::
 +
 Whether to allow automatic synchronization of an account's inactive flag upon login.
++
 If set to true, upon login, if the authentication back-end reports the account as active,
-the account's inactive flag in the internal Gerrit database will be updated to be active.
+the account's inactive flag in NoteDb will be updated to be active.
++
 If the authentication back-end reports the account as inactive, the account's flag will be
 updated to be inactive and the login attempt will be blocked. Users enabling this feature
 should ensure that their authentication back-end is supported. Currently, only
@@ -693,8 +695,11 @@
 
 [[auth.skipFullRefEvaluationIfAllRefsAreVisible]]auth.skipFullRefEvaluationIfAllRefsAreVisible::
 +
-Whether to skip the full ref visibility checks as a performance shortcut when all refs are
-visible to a user. Full ref filtering would filter out things like pending edits.
+Whether to skip the full ref visibility checks as a performance shortcut when a
+user has READ permission for all refs.
++
+The full ref filtering would filter out refs for pending edits, private changes
+and auto merge commits.
 +
 By default, true.
 
@@ -751,6 +756,24 @@
 +
 Default is false.
 
+[[cache.openFiles]]cache.openFiles::
++
+The number of file descriptors to add to the limit set by the Gerrit daemon.
++
+Persistent caches are stored on the file system and as such participate in the
+file descriptors utilization. The number of file descriptors can vary depending
+on the cache configuration and the specific backend used.
++
+The additional file descriptors required by the cache should be accounted for
+via this setting, so that the Gerrit daemon can adjust the ulimit accordingly.
++
+If you increase this to a larger setting you may need to also adjust
+the ulimit on file descriptors for the host JVM, as Gerrit needs
+additional file descriptors available for network sockets and other
+repository data manipulation.
++
+Default is 0.
+
 [[cache.name.maxAge]]cache.<name>.maxAge::
 +
 Maximum age to keep an entry in the cache. Entries are removed from
@@ -833,10 +856,54 @@
 * `"change_notes"`: disk storage is disabled by default
 * `"diff_summary"`: default is `1g` (1 GiB of disk space)
 * `"external_ids_map"`: disk storage is disabled by default
+* `"persisted_projects"`: default is `1g` (1 GiB of disk space)
 
 +
 If 0 or negative, disk storage for the cache is disabled.
 
+[[cache.name.expireAfterWrite]]cache.<name>.expireAfterWrite::
++
+Duration after which a cached value will be evicted and not
+read anymore.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+Disabled by default.
+
+[[cache.name.refreshAfterWrite]]cache.<name>.refreshAfterWrite::
++
+Duration after which we asynchronously refresh the cached value.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory. Defaults to 15 minutes.
+
+[[cache.refreshThreadPoolSize]]cache.refreshThreadPoolSize::
++
+Number of threads that are available to refresh cached values that became
+out of date. This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory
++
+Refreshes will only be scheduled on this executor if the values are
+out of sync.
+The check if they are is cheap and always happens on the thread that
+inquires for a cached value.
++
+Defaults to 2.
+
 ==== [[cache_names]]Standard Caches
 
 cache `"accounts"`::
@@ -873,8 +940,8 @@
 this cache should be disabled in a cluster setup using multiple primary
 or multiple replica nodes.
 +
-The cache should be flushed whenever the database changes table is modified
-outside of Gerrit.
+The cache should be flushed whenever NoteDb change metadata in a repository is
+modified outside of Gerrit.
 
 cache `"diff"`::
 +
@@ -1031,10 +1098,17 @@
 has been converted from Markdown to HTML. The memoryLimit refers to
 the bytes of memory dedicated to storing the documentation.
 
+cache `"persisted_projects"`::
++
+Caches the project description records, from the `refs/meta/config`
+branch of each project. This is the persisted variant of the
+`projects` cache. The intention is for this cache to have an in-memory
+size of 0.
+
 cache `"projects"`::
 +
-Caches the project description records, from the `projects` table
-in the database.  If a project record is updated or deleted, this
+Caches the project description records, from the `refs/meta/config`
+branch of each project. If a project record is updated or deleted, this
 cache should be flushed.  Newly inserted projects do not require
 a cache flush, as they will be read upon first reference.
 
@@ -1134,22 +1208,6 @@
 +
 Default is true, enabled.
 
-[[cache.projects.checkFrequency]]cache.projects.checkFrequency::
-+
-How often project configuration should be checked for update from Git.
-Gerrit Code Review caches project access rules and configuration in
-memory, checking the refs/meta/config branch every checkFrequency
-minutes to see if a new revision should be loaded and used for future
-access. Values can be specified using standard time unit abbreviations
-('ms', 'sec', 'min', etc.).
-+
-If set to 0, checks occur every time, which may slow down operations.
-If set to 'disabled' or 'off', no check will ever be done.
-Administrators may force the cache to flush with
-link:cmd-flush-caches.html[gerrit flush-caches].
-+
-Default is 5 minutes.
-
 [[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
 +
 If the project cache should be loaded during server startup.
@@ -1201,7 +1259,7 @@
 Whether the first user that logs in to the Gerrit server should
 automatically be added to the administrator group and hence get the
 `administrateServer` capability assigned. This is useful to bootstrap
-the authentication database.
+the link:config-accounts.html[account data].
 +
 Default is true.
 
@@ -1258,14 +1316,14 @@
 If set to true, then all UI features for using and interacting with the
 attention set are enabled.
 +
-The default is false for now, but will be changed to true in Q2 2020.
+The default is true.
 
 [[change.enableAssignee]]change.enableAssignee::
 +
 If set to true, then all UI features for using and interacting with the
 assignee are enabled.
 +
-The default is true for now, but will be changed to false in Q2 2020.
+The default is false.
 
 [[change.largeChange]]change.largeChange::
 +
@@ -1374,6 +1432,14 @@
 +
 The default limit is 1MiB.
 
+[[change.sendNewPatchsetEmails]]change.sendNewPatchsetEmails::
++
+When false, emails will not be sent to owners, reviewers, and cc for
+creating a new patchset unless they are project watchers or have starred
+the change.
++
+Default is true.
+
 [[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
 +
 Show assignee field in changes table. If set to false, assignees will
@@ -2068,7 +2134,7 @@
 or "http://example.com:8080/gerrit/" so Gerrit can output links that point
 back to itself.
 +
-Setting this is highly recommended, as its necessary for the upload
+Setting this is highly recommended, as it is necessary for the upload
 code invoked by "git push" or "repo upload" to output hyperlinks
 to the newly uploaded changes.
 
@@ -2129,10 +2195,20 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.installBatchModule]]gerrit.installBatchModule::
++
+Repeatable list of class name of additional Guice modules to load as
+override to the batchInjector's modules during the init phases.
+Classes are resolved using the primary Gerrit class loader, hence the
+class needs to be either declared in Gerrit or an additional JAR
+located under the `/lib` directory.
++
+By default unset.
+
 [[gerrit.installDbModule]]gerrit.installDbModule::
 +
 Repeatable list of class name of additional Guice modules to load at
-Gerrit startup as part of the dbInjector and during the init phases.
+Gerrit startup as part of the dbInjector.
 Classes are resolved using the primary Gerrit class loader, hence the
 class needs to be either declared in Gerrit or an additional JAR
 located under the `/lib` directory.
@@ -2142,7 +2218,7 @@
 [[gerrit.installModule]]gerrit.installModule::
 +
 Repeatable list of class name of additional Guice modules to load at
-Gerrit startup as part of the sysInjector and during the init phases.
+Gerrit startup as part of the sysInjector.
 Classes are resolved using the primary Gerrit class loader, hence the
 class needs to be either declared in Gerrit or an additional JAR
 located under the `/lib` directory.
@@ -2155,6 +2231,7 @@
   installModule = com.googlesource.gerrit.libmodule.MyModule
   installModule = com.example.abc.OurSpecialSauceModule
   installDbModule = com.example.def.OurCustomProvider
+  installBatchModule = com.example.ghi.CustomBatchInitModule
 ----
 
 [[gerrit.listProjectsFromIndex]]gerrit.listProjectsFromIndex::
@@ -2270,8 +2347,8 @@
 [[gerrit.experimentalRollingUpgrade]]gerrit.experimentalRollingUpgrade::
 +
 Enable Gerrit rolling upgrade to the next version.
-For example if Gerrit v3.1 is version N (All-Projects:refs/meta/version=181)
-then its next version N+1 is v3.2 (All-Projects:refs/meta/version=183).
+For example if Gerrit v3.2 is version N (All-Projects:refs/meta/version=183)
+then its next version N+1 is v3.3 (All-Projects:refs/meta/version=184).
 Allow Gerrit to start even if the underlying schema version has been bumped to
 the next Gerrit version.
 +
@@ -2288,7 +2365,7 @@
 1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
 2. Set the first master unhealthy
 3. Shutdown the first master and [upgrade](install.html#init) to the next version
-4. Startup the first master, wait for the online reindex to complete
+4. Startup the first master, wait for the online reindex to complete (where applicable)
 5. Verify the the first master upgrade is successful and online reindex is complete
 6. Set the first master healthy
 7. Repeat steps 2. to 6. for all the other Gerrit nodes
@@ -3308,6 +3385,49 @@
 config is backwards compatible with what the default was before the config
 was added.
 
+[[event.comment-added.publishPatchSetLevelComment]]event.comment-added.publishPatchSetLevelComment::
++
+Add patch set level comment as event comment. Without this option, patch set
+level comment will not be included in the event comment attribute. Given that
+currently patch set level, file and robot comments are not exposed in the
+`comment-added` event type, those comments will be lost. One particular use
+case is to re-trigger CI build from the change screen by adding a comment with
+specific content, e.g.: `recheck`. Jenkins Gerrit Trigger plugin and Zuul CI
+depend on this feature to trigger change verification.
++
+By default, true.
+
+[[experiments]]
+=== Section experiments
+
+This section covers experimental new features. Gerrit's frontend uses experiments
+to research new behavior. Once the research is done, the experimental feature
+either stays and the experimentation flag gets removed, or the feature as a whole
+gets removed
+
+[[experiments.enabled]]experiments.enabled::
++
+List of experiments that are currently enabled. The release notes contain currently
+available experiments.
++
+We will not remove experiments in stable patch releases. They are likely to be
+removed in the next stable version.
+
+----
+[experiments]
+  enabled = ExperimentKey
+----
+
+[[experiments.disabled]]experiments.disabled::
++
+List of experiments that are currently disabled. The release notes contain currently
+available experiments. This list disables experiments with the given key that are
+either enabled by default or explicitly in the config.
+
+----
+[experiments]
+  disabled = ExperimentKey
+----
 
 [[ldap]]
 === Section ldap
@@ -3359,6 +3479,11 @@
 If `auth.type` is `LDAP` this setting should use `ldaps://` to
 ensure the end user's plaintext password is transmitted only over
 an encrypted connection.
++
+If you want to configure multiple ldap servers you can try to put
+multiple ldap urls separated by a space:
+`server = ldaps://ldap1 ldaps://ldap2`
+See https://bugs.chromium.org/p/gerrit/issues/detail?id=10841[issue 10841].
 
 [[ldap.startTls]]ldap.startTls::
 +
@@ -3971,18 +4096,6 @@
 +
 Default is 5 minutes.
 
-[[receive.changeUpdateThreads]]receive.changeUpdateThreads::
-+
-Number of threads to perform change creation or patch set updates
-concurrently. Each thread uses its own database connection from
-the database connection pool, and if all threads are busy then
-main receive thread will also perform a change creation or patch
-set update.
-+
-Defaults to 1, using only the main receive thread. This feature is for
-databases with very high latency that can benefit from concurrent
-operations when multiple changes are impacted at once.
-
 [[receive.checkMagicRefs]]receive.checkMagicRefs::
 +
 If true, Gerrit will verify the destination repository has
@@ -4204,9 +4317,8 @@
 
 [[repository.name.ownerGroup]]repository.<name>.ownerGroup::
 +
-A name of a group which exists in the database. Zero, one or many
-groups are allowed.  Each on its own line.  Groups which don't exist
-in the database are ignored.
+A name of a link:config-groups.html[group] which exists. Zero, one or many
+groups are allowed.  Each on its own line.  Groups which don't exist are ignored.
 
 [[retry]]
 === Section retry
@@ -4390,19 +4502,25 @@
 
 [[receiveemail.filter.mode]]receiveemail.filter.mode::
 +
-A black- and whitelist filter to filter incoming emails.
+An allow and block filter to filter incoming emails.
 +
 If `OFF`, emails are not filtered by the list filter.
 +
-If `WHITELIST`, only emails where a pattern from
+If `ALLOW`, only emails where a pattern from
 <<receiveemail.filter.patterns,receiveemail.filter.patterns>>
 matches 'From' will be processed.
 +
-If `BLACKLIST`, only emails where no pattern from
+If `BLOCK`, only emails where no pattern from
 <<receiveemail.filter.patterns,receiveemail.filter.patterns>>
 matches 'From' will be processed.
 +
 Defaults to `OFF`.
++
+The previous filter-names 'BLACKLIST' and 'WHITELIST' have been deprecated
+since they may be considered disrespectful and there's no technical or
+practical reason to use these exact terms for the filters.
+For backwards compatibility they are still supported but support for these
+deprecated terms will be removed in future releases.
 
 [[receiveemail.filter.patterns]]receiveemail.filter.patterns::
 +
@@ -4533,9 +4651,10 @@
 
 [[sendemail.allowrcpt]]sendemail.allowrcpt::
 +
-If present, each value adds one entry to the whitelist of email
-addresses that Gerrit can send email to.  If set to a complete
-email address, that one address is added to the white list.
+If present, each value adds one entry to the list of allowed email
+addresses that Gerrit can send emails to.  If set to a complete
+email address, that one address is added to the list of allowed
+emails.
 If set to a domain name, any address at that domain can receive
 email from Gerrit.
 +
@@ -4546,9 +4665,10 @@
 
 [[sendemail.denyrcpt]]sendemail.denyrcpt::
 +
-If present, each value adds one entry to the blacklist of email
-addresses that Gerrit can send email to.  If set to a complete
-email address, that one address is added to the blacklist.
+If present, each value adds one entry to the list of email
+addresses that Gerrit can't send emails to.  If set to a complete
+email address, that one address is added to the list of blocked
+emails.
 If set to a domain name, any address at that domain can *not* receive
 email from Gerrit.
 +
@@ -4747,16 +4867,16 @@
 [[sshd.batchThreads]]sshd.batchThreads::
 +
 Number of threads to allocate for SSH command requests from
-link:access-control.html#non-interactive_users[non-interactive users].
+link:access-control.html#service_users[service users].
 If equals to 0, then all non-interactive requests are executed in the same
 queue as interactive requests.
 +
 Any other value will remove the number of threads from the queue
 allocated to interactive users, and create a separate thread pool
 of the requested size, which will be used to run commands from
-non-interactive users.
+service users.
 +
-If the number of threads requested for non-interactive users is larger
+If the number of threads requested for service users is larger
 than the total number of threads allocated in sshd.threads, then the
 value of sshd.threads is increased to accommodate the requested value.
 +
@@ -5022,6 +5142,7 @@
 used for suggesting accounts when adding members to a group.
 +
 By default 0.
+
 [[suggest.relevantChanges]]suggest.relevantChanges::
 +
 When suggesting reviewers, we go over recent changes of the user, and
@@ -5034,6 +5155,13 @@
 +
 By default 50.
 
+[[suggest.skipServiceUsers]]suggest.skipServiceUsers::
++
+If link:access-control.html#service_users[service users] should be skipped when
+suggesting reviewers.
++
+By default true.
+
 [[tracing]]
 === Section tracing
 
@@ -5414,10 +5542,6 @@
 [auth]
   registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
 
-[database]
-  username = webuser
-  password = s3kr3t
-
 [ldap]
   password = l3tm3srch
 
@@ -5537,10 +5661,12 @@
 
 [[receive.autogc]]receive.autogc::
 +
-By default, `git-receive-pack` will run auto gc after receiving data from git-push and updating refs.
+By default, up to Gerrit 3.2 `git-receive-pack` will run auto gc after receiving data from git-push and updating refs.
 You can stop it by setting this variable to `false`. This is recommended in gerrit to avoid the
 additional load this creates. Instead schedule gc using link:cmd-gc.html#gc.startTime[gc.startTime]
 and link:cmd-gc.html#gc.interval[gc.interval] or e.g. in a cron job that runs gc in a separate process.
+Since Gerrit 3.3 the init command will auto-configure `git-receive-pack = false` in `etc/jgit.config` if
+it wasn't set manually and show a warning if it was set to `true` manually.
 
 GERRIT
 ------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 5d22c0b..85006dc 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -109,10 +109,10 @@
 reverting a change.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
 ChangeFooter.
 
-=== RegisterNewEmail.soy
+=== RegisterNewEmail.soy and RegisterNewEmailHtml.soy
 
-The `RegisterNewEmail.soy` template will determine the contents of the email
-related to registering new email accounts.
+Those templates will determine the contents of the email related to registering
+new email accounts.
 
 === ReplacePatchSet.soy and ReplacePatchSetHtml.soy
 
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 564b2a6..5d71b41 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -18,10 +18,10 @@
 To build Gerrit from source, you need:
 
 * A Linux or macOS system (Windows is not supported at this time)
-* A JDK for Java 8|9|10|11|...
+* A JDK for Java 8|11|...
 * Python 3
 * link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm),role=external,window=_blank]
-* Bower (`sudo npm install -g bower`)
+* Bower (`npm install -g bower`)
 * link:https://docs.bazel.build/versions/master/install.html[Bazel,role=external,window=_blank] -launched with
 link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank]
 * Maven
@@ -32,29 +32,41 @@
 [[bazel]]
 === Bazel
 
-link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] includes a
-link:https://bazel.build/[Bazel,role=external,window=_blank] version check and downloads the correct
-`bazel` version for the git project/repository. Bazelisk is the recommended
-`bazel` launcher for Gerrit. Once Bazelisk is installed locally, a `bazel`
-symlink can be created towards it. This is so that every `bazel` command
-seamlessly uses Bazelisk, which then runs the proper `bazel` binary version.
+link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] is a version
+manager for link:https://bazel.build/[Bazel,role=external,window=_blank], similar to how `nvm`
+manages `npm` versions. It takes care of downloading and installing Bazel itself, so you don't have
+to worry about using the correct version of Bazel. Bazelisk can be installed in different
+ways: link:https://docs.bazel.build/install-bazelisk.html[Install,role=external,window=_blank]
 
 [[java]]
 === Java
 
-==== MacOS
-
-On MacOS, ensure that "Java for MacOS X 10.5 Update 4" (or higher) is installed
-and that `JAVA_HOME` is set to the
-link:install.html#Requirements[required Java version].
-
-Java installations can typically be found in
-"/System/Library/Frameworks/JavaVM.framework/Versions".
+Ensure that the link:install.html#Requirements[required Java version]
+is installed and that `JAVA_HOME` is set to it.
 
 To check the installed version of Java, open a terminal window and run:
 
 `java -version`
 
+[[java-8]]
+==== Java 8 support (deprecated)
+
+Java 8 is a legacy Java release and support for Java 8 will be discontinued
+in future gerrit releases. To build Gerrit with Java 8 language level, run:
+
+```
+  $ bazel build :release
+```
+
+[[java-11]]
+==== Java 11 support
+
+To build Gerrit with Java 11 language level, run:
+
+```
+  $ bazel build --java_toolchain=//tools:error_prone_warnings_toolchain_java11 :release
+```
+
 [[java-13]]
 ==== Java 13 support
 
@@ -102,22 +114,6 @@
 Now, invoking Bazel with just `bazel build :release` would include
 all those options.
 
-[[java-11]]
-==== Java 11 support
-
-Java 11 is supported through alternative java toolchain
-link:https://docs.bazel.build/versions/master/toolchains.html[Bazel option].
-To build Gerrit with Java 11, specify JDK 11 java toolchain:
-
-```
-  $ bazel build \
-      --host_javabase=@bazel_tools//tools/jdk:remote_jdk11 \
-      --javabase=@bazel_tools//tools/jdk:remote_jdk11 \
-      --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 \
-      --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 \
-      :release
-```
-
 === Node.js and npm packages
 See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/README.md#installing-node_js-and-npm-packages[Installing Node.js and npm packages,role=external,window=_blank].
 
@@ -272,7 +268,15 @@
   bazel-bin/Documentation/searchfree.zip
 ----
 
-To build the executable WAR with the documentation included:
+To generate HTML files skipping the zip archiving:
+
+----
+  bazel build Documentation
+----
+
+And open `bazel-bin/Documentation/index.html`.
+
+To build the Gerrit executable WAR with the documentation included:
 
 ----
   bazel build withdocs
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 23ecd67..477641b 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -144,7 +144,7 @@
   and everyone can comment and raise concerns.
 * Design docs should stay open for a minimum of 10 calendar days so
   that everyone has a fair chance to join the review.
-* Within 14 calendar days the contributor should hear back from the
+* Within 30 calendar days the contributor should hear back from the
   link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank]
   whether the proposed feature is in scope of the project and if it can
   be accepted.
diff --git a/Documentation/dev-core-plugins.txt b/Documentation/dev-core-plugins.txt
index 04e2420..6b777d3 100644
--- a/Documentation/dev-core-plugins.txt
+++ b/Documentation/dev-core-plugins.txt
@@ -155,6 +155,52 @@
    link:https://www.gerritcodereview.com/news.html[project news].
 --
 
+[[removing]]
+=== Removing Core Plugins
+
+A core plugin could be subject to NOT be considered core anymore if:
+
+1. Does not respect the license:
++
+The plugin code or the libraries used are not following anymore the
+Apache License Version 2.0.
+
+2. Is out of scope:
++
+The plugin functionality has gone outside the Gerrit-related scope,
+has a clear scope or conflict with other core plugins or existing and
+planned Gerrit core features.
++
+NOTE: The plugin would need to remain core until the planned replacement gets
+implemented. Otherwise the feature is likely missing between the removal and
+planned implementation times.
+
+3. Is not relevant:
++
+The plugin functionality is no more relevant to a majority of the Gerrit community:
++
+--
+** An out of the box Gerrit installation won’t be missing anything if the plugin is
+   not installed.
+** It isn’t anymore used by most sites.
+** Multiple parties (different organizations/companies) have abandoned the use of
+   the plugin and agree that it should not be anymore a core plugin.
+** If the same or similar functionality is provided by multiple plugins, the plugin
+   is not a clear recommended solution anymore by the community.
+** Whether a plugin is no more relevant to a majority of the Gerrit community must be
+   discussed on a case-by-case basis. In case of doubt, it’s up to the engineering
+   steering committee to make a decision.
+--
+
+4. Degraded code quality:
++
+The plugin code maintenance is lacking and has not anymore good test coverage.
+Maintaining the plugin code creates a significant overhead for the Gerrit maintainers.
+
+5. Outdated documentation:
++
+The plugin functionality documented is significantly outdated.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
index 7e48eea..6dc6f5f 100644
--- a/Documentation/dev-design-docs.txt
+++ b/Documentation/dev-design-docs.txt
@@ -132,7 +132,7 @@
 
 For proposed features the contributor should hear back from the
 link:dev-processes.html#steering-committee[engineering steering
-committee] within 14 calendar days whether the proposed feature is in
+committee] within 30 calendar days whether the proposed feature is in
 scope of the project and if it can be accepted.
 
 [[meetings]]
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 742cf42..bbe227a 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -4,7 +4,8 @@
 This document is about configuring Gerrit Code Review into an
 Eclipse workspace for development.
 
-Java 8 or later SDK is required.
+Java 11 or later SDK is required.
+Otherwise, java 8 can still be used for now as described below.
 
 [[setup]]
 == Project Setup
@@ -30,6 +31,10 @@
 ----
 
 First, generate the Eclipse project by running the `tools/eclipse/project.py` script.
+If running Eclipse on Java 8, add the extra parameter
+`-e='--java_toolchain=//tools:error_prone_warnings_toolchain'`
+for generating a compatible project.
+
 Then, in Eclipse, choose 'Import existing project' and select the `gerrit` project
 from the current working directory.
 
@@ -79,15 +84,16 @@
 link:dev-build-plugins.html#_bundle_custom_plugin_in_release_war[bundling in release.war]
 and run `tools/eclipse/project.py`.
 
-[[Newer Java versions]]
+== Java Versions
 
-Java 9 and later are supported, but some adjustments must be done, because
-Java 8 is still the default:
+Java 11 is supported as a default, but some adjustments must be done for other JDKs:
 
 * Add JRE, e.g.: directory: /usr/lib64/jvm/java-9-openjdk, name: java-9-openjdk-9
 * Change execution environment for gerrit project to: JavaSE-9 (java-9-openjdk-9)
 * Check that compiler compliance level in gerrit project is set to: 9
 
+Moreover, the actual java 11 language features are not supported yet.
+
 [[Formatting]]
 == Code Formatter Settings
 
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index b67d546..149b14a 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -9,7 +9,7 @@
 <<dev-bazel#installation,Building with Bazel - Installation>>.
 
 It's strongly recommended to verify you can build your Gerrit tree with Bazel
-for Java 8 from the command line first. Ensure that at least
+for Java 11 from the command line first. Ensure that at least
 `bazel build gerrit` runs successfully before you proceed.
 
 === IntelliJ version and Bazel plugin
@@ -21,12 +21,12 @@
 Also note that the version of the Bazel plugin used in turn may or may not be
 compatible with the Bazel version used.
 
-In addition, Java 8 must be specified on your path or via `JAVA_HOME` so that
+In addition, Java 11 must be specified on your path or via `JAVA_HOME` so that
 building with Bazel via the Bazel plugin is possible.
 
 TIP: If the synchronization of the project with the BUILD files using the Bazel
 plugin fails and IntelliJ reports the error **Could not get Bazel roots**, this
-indicates that the Bazel plugin couldn't find Java 8.
+indicates that the Bazel plugin couldn't find Java 11.
 
 === Installation of IntelliJ IDEA
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 9a5ce03..03e8ce6 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -379,13 +379,13 @@
 notifications of these events by implementing the corresponding
 listeners.
 
-* `com.google.gerrit.common.EventListener`:
+* `com.google.gerrit.server.events.EventListener`:
 +
 Allows to listen to events without user visibility restrictions. These
 are the same link:cmd-stream-events.html#events[events] that are also streamed by
 the link:cmd-stream-events.html[gerrit stream-events] command.
 
-* `com.google.gerrit.common.UserScopedEventListener`:
+* `com.google.gerrit.server.events.UserScopedEventListener`:
 +
 Allows to listen to events visible to the specified user. These are the
 same link:cmd-stream-events.html#events[events] that are also streamed
@@ -745,7 +745,8 @@
 Plugin methods implementing search operands (returning a
 `Predicate<ChangeData>`), must be defined on a class implementing
 one of the `ChangeQueryBuilder.ChangeOperandsFactory` interfaces
-(.e.g., ChangeQueryBuilder.ChangeHasOperandFactory).  The specific
+(.e.g., ChangeQueryBuilder.ChangeHasOperandFactory or
+ChangeQueryBuilder.ChangeIsOperandFactory).  The specific
 `ChangeOperandFactory` class must also be bound to the `DynamicSet` from
 a module's `configure()` method in the plugin.
 
@@ -926,13 +927,15 @@
 [[query_attributes]]
 == Change Attributes
 
+=== ChangePluginDefinedInfoFactory
+
 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.
+Query Change APIs by implementing the `ChangePluginDefinedInfoFactory` 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 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
@@ -944,7 +947,7 @@
   @Override
   protected void configure() {
     // Register attribute factory.
-    DynamicSet.bind(binder(), ChangeAttributeFactory.class)
+    DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
         .to(AttributeFactory.class);
 
     // Register options for GET /changes/X/change and /changes/X/detail.
@@ -969,7 +972,7 @@
   public boolean all = false;
 }
 
-public class AttributeFactory implements ChangeAttributeFactory {
+public class AttributeFactory implements ChangePluginDefinedInfoFactory {
   protected MyChangeOptions options;
 
   public class PluginAttribute extends PluginDefinedInfo {
@@ -983,14 +986,17 @@
   }
 
   @Override
-  public PluginDefinedInfo create(ChangeData c, BeanProvider bp, String plugin) {
+  public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+      Collection<ChangeData> cds, BeanProvider bp, String plugin) {
     if (options == null) {
       options = (MyChangeOptions) bp.getDynamicBean(plugin);
     }
+    Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
     if (options.all) {
-      return new PluginAttribute(c);
+      cds.forEach(cd -> out.put(cd.getId(), new PluginAttribute(cd)));
+      return out;
     }
-    return null;
+    return ImmutableMap.of();
   }
 }
 ----
@@ -1025,10 +1031,20 @@
 }
 ----
 
-Implementors of the `ChangeAttributeFactory` interface should check whether
-they need to contribute to the link:#change-etag-computation[change ETag
-computation] to prevent callers using ETags from potentially seeing outdated
-plugin attributes.
+Runtime exceptions generated by the implementors of ChangePluginDefinedInfoFactory
+are encapsulated in PluginDefinedInfo objects which are part of SSH/REST query output.
+
+=== ChangeAttributeFactory
+
+Alternatively, there is also `ChangeAttributeFactory` which takes in one single
+`ChangeData` at a time. `ChangePluginDefinedInfoFactory` should be preferred
+over this as it handles many changes at once which also decreases the round-trip
+time for queries resulting in performance increase for bulk queries.
+
+Implementors of the `ChangePluginDefinedInfoFactory` and `ChangeAttributeFactory`
+interfaces should check whether they need to contribute to the
+link:#change-etag-computation[change ETag computation] to prevent callers using
+ETags from potentially seeing outdated plugin attributes.
 
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
@@ -2705,7 +2721,7 @@
 Plugins may also decide not to vote on a given change by returning an
 `Optional.empty()` (ie: the plugin is not enabled for this repository).
 
-If a plugin decides not to vote, it's name will not be displayed in the UI and
+If a plugin decides not to vote, its name will not be displayed in the UI and
 it will not be recoded in the database.
 
 .Gerrit's Pre-submit handling with three plugins
@@ -2739,8 +2755,8 @@
 [source, java]
 ----
 import java.util.Optional;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.common.data.SubmitRecord.Status;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Status;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.SubmitRule;
 
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 0fe2372..5049831 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -15,7 +15,7 @@
 * ensuring timely design reviews
 * ensuring that new features are compatible with the project vision and
   are well aligned with other features (give feedback on new
-  link:dev-design-docs.html[design docs] within 14 calendar days)
+  link:dev-design-docs.html[design docs] within 30 calendar days)
 * approving/rejecting link:dev-design-docs.html[designs], vetoing new
   features
 * assigning link:dev-roles.html#mentor[mentors] for approved features
@@ -278,14 +278,32 @@
 test that verifies that the security vulnerability is no longer present.
 +
 Review and approval of the security fixes must be done by the Gerrit
-maintainers. Verifications must be done manually since the Gerrit CI doesn't
-build and test changes of the `gerrit-security-fixes` repository (and it
-shouldn't because everything on the CI server is public which would break
-the embargo).
+maintainers.
 +
 Once a security fix is ready and submitted, it should be cherry-picked to all
 branches that should be fixed.
 
+. CI validation of the security fix:
++
+The validation of the security fixes does not happen on the regular Gerrit CI,
+because it would compromise the confidentiality of the fix and therefore break
+the embargo.
++
+The release manager maintains a private branch on the
+link:https://gerrit-review.googlesource.com/admin/repos/gerrit-ci-scripts[gerrit-ci-scripts,role=external,window=_blank] repository
+which contains a special build pipeline with special visibility restrictions.
++
+The validation process provides feedback, in terms of Code-Style, Verification
+and Checks, to the incoming security changes. The links associated
+with the build logs are exposed over the Internet but their access limited
+to only those who are actively participating in the development and review of
+the security fix.
++
+The maintainers that are willing to access the links to the CI logs need
+to request a time-limited (maximum 30 days) nominal X.509 certificate from a
+CI maintainer, which allows to access the build logs and analyze failures.
+The release manager may help obtaining that certificate from CI maintainers.
+
 . Creation of fixed releases and announcement of the security vulnerability:
 +
 A release manager should create new bug fix releases for all fixed branches.
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index a4ccccf..db08da5 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -44,15 +44,13 @@
 +
 Generate and publish a PGP key as described in
 link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[
-Working with PGP Signatures,role=external,window=_blank]. In addition to the keyserver mentioned
-there it is recommended to also publish the key to the
-link:https://keyserver.ubuntu.com/[Ubuntu key server].
+Working with PGP Signatures,role=external,window=_blank].
 +
 Please be aware that after publishing your public key it may take a
 while until it is visible to the Sonatype server.
 +
 Add an entry for the public key in the
-link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list,role=external,window=_blank]
+link:https://gerrit.googlesource.com/homepage/+/master/pages/site/releases/public-keys.md[key list,role=external,window=_blank]
 on the homepage.
 +
 The PGP passphrase can be put in `~/.m2/settings.xml`:
diff --git a/Documentation/dev-roles.txt b/Documentation/dev-roles.txt
index 2ca7f22..cecaedc 100644
--- a/Documentation/dev-roles.txt
+++ b/Documentation/dev-roles.txt
@@ -213,10 +213,10 @@
 [[maintainer-election]]
 Maintainers can nominate new maintainers by posting a nomination on the
 non-public maintainers mailing list. Nominations should stay open for
-at least 14 calendar days so that all maintainers have a chance to
+at least 10 calendar days so that all maintainers have a chance to
 vote. To be approved as maintainer a minimum of 5 positive votes and no
 negative votes is required. This means if 5 positive votes without
-negative votes have been reached and 14 calendar days have passed, any
+negative votes have been reached and 10 calendar days have passed, any
 maintainer can close the vote and welcome the new maintainer. Extending
 the voting period during holiday season or if there are not enough
 votes is possible, but the voting period should not exceed 1 month. If
diff --git a/Documentation/images/user-attention-set-dashboard-empty.png b/Documentation/images/user-attention-set-dashboard-empty.png
new file mode 100644
index 0000000..7b15fa0
--- /dev/null
+++ b/Documentation/images/user-attention-set-dashboard-empty.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-dashboard.png b/Documentation/images/user-attention-set-dashboard.png
new file mode 100644
index 0000000..4533380
--- /dev/null
+++ b/Documentation/images/user-attention-set-dashboard.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-hovercard.png b/Documentation/images/user-attention-set-hovercard.png
new file mode 100644
index 0000000..8d6af58
--- /dev/null
+++ b/Documentation/images/user-attention-set-hovercard.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-icon-click.png b/Documentation/images/user-attention-set-icon-click.png
new file mode 100644
index 0000000..32b1961
--- /dev/null
+++ b/Documentation/images/user-attention-set-icon-click.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-icon.png b/Documentation/images/user-attention-set-icon.png
new file mode 100644
index 0000000..a6789b9
--- /dev/null
+++ b/Documentation/images/user-attention-set-icon.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-reply-modify.png b/Documentation/images/user-attention-set-reply-modify.png
new file mode 100644
index 0000000..a8895f9
--- /dev/null
+++ b/Documentation/images/user-attention-set-reply-modify.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-reply-select.png b/Documentation/images/user-attention-set-reply-select.png
new file mode 100644
index 0000000..e93ff58
--- /dev/null
+++ b/Documentation/images/user-attention-set-reply-select.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-user-prefs.png b/Documentation/images/user-attention-set-user-prefs.png
new file mode 100644
index 0000000..47cdbf5
--- /dev/null
+++ b/Documentation/images/user-attention-set-user-prefs.png
Binary files differ
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 94a576c..6e1a9bd 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -10,39 +10,6 @@
 +
 Gerrit is not yet compatible with Java 13 or newer at this time.
 
-[[cryptography]]
-== Configure Java for Strong Cryptography
-
-Support for extra strength cryptographic ciphers: _AES128CTR_, _AES256CTR_,
-_ARCFOUR256_, and _ARCFOUR128_ can be enabled by downloading the _Java
-Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files_
-from Oracle and installing them into your JRE.
-
-[NOTE]
-Installing JCE extensions is optional and export restrictions may apply.
-
-. Download the unlimited strength JCE policy files.
-+
-- link:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html[JDK7 JCE policy files,role=external,window=_blank]
-- link:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html[JDK8 JCE policy files,role=external,window=_blank]
-. Uncompress and extract the downloaded file.
-+
-The downloaded file  contains the following files:
-+
-[cols="2"]
-|===
-|README.txt
-|Information about JCE and installation guide
-
-|local_policy.jar
-|Unlimited strength local policy file
-
-|US_export_policy.jar
-|Unlimited strength US export policy file
-|===
-. Install the unlimited strength policy JAR files by following instructions
-found in `README.txt`.
-
 [[download]]
 == Download Gerrit
 
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt
index 92732d0..6565ba4 100644
--- a/Documentation/intro-gerrit-walkthrough.txt
+++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -296,6 +296,10 @@
 * Review the link:intro-project-owner.html[Project Owners guide] to learn more
   about configuring projects in Gerrit, including setting user permissions and
   configuring verification checks
+* Read through the Git and Gerrit training slides that explain concepts and
+  workflows in detail. They are meant for self-studying how Git and Gerrit work:
+** link:https://docs.google.com/presentation/d/1IQCRPHEIX-qKo7QFxsD3V62yhyGA9_5YsYXFOiBpgkk/edit?usp=sharing[Git explained: Git Concepts and Workflows]
+** link:https://docs.google.com/presentation/d/1C73UgQdzZDw0gzpaEqIC6SPujZJhqamyqO1XOHjH-uk/edit?usp=sharing[Gerrit explained: Gerrit Concepts and Workflows]
 
 GERRIT
 ------
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 4bc386a..ae494dd 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -247,209 +247,15 @@
 ----
 
 
-[[ba-linkify]]
-ba-linkify
+[[Polymer-2014]]
+Polymer-2014
 
-* ba-linkify
+* @polymer/paper-ripple
+* @polymer/paper-styles
 
-[[ba-linkify_license]]
+[[Polymer-2014_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
-
-* 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.
-
-----
-
-
-[[isarray]]
-isarray
-
-* isarray
-
-[[isarray_license]]
-----
-(MIT)
-
-Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
-
-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
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation 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.
-
-----
-
-
-[[page]]
-page
-
-* page
-
-[[page_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.
-
-----
-
-
-[[path-to-regexp]]
-path-to-regexp
-
-* path-to-regexp
-
-[[path-to-regexp_license]]
-----
-The MIT License (MIT)
-
-Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
-
-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-2018]]
-Polymer-2018
-
-* @webcomponents/webcomponentsjs
-* polymer-bridges
-* polymer-resin
-
-[[Polymer-2018_license]]
-----
-Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
+Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
 
 This code may only be used under the BSD style license found at
 http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
@@ -487,250 +293,6 @@
 ----
 
 
-[[shadow-selection-polyfill]]
-shadow-selection-polyfill
-
-* shadow-selection-polyfill
-
-[[shadow-selection-polyfill_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.
-
-----
-
-
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-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.
-
-----
-
-
 [[Polymer-2015]]
 Polymer-2015
 
@@ -818,6 +380,133 @@
 ----
 
 
+[[Polymer-2017]]
+Polymer-2017
+
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
+
+[[Polymer-2017_license]]
+----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+
+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.
+
+----
+
+
+[[Polymer-2018]]
+Polymer-2018
+
+* @webcomponents/webcomponentsjs
+* polymer-bridges
+* polymer-resin
+
+[[Polymer-2018_license]]
+----
+Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
+
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+
+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.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* 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.
+
+----
+
+
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
@@ -1271,94 +960,99 @@
 ----
 
 
-[[Polymer-2014]]
-Polymer-2014
+[[isarray]]
+isarray
 
-* @polymer/paper-ripple
-* @polymer/paper-styles
+* isarray
 
-[[Polymer-2014_license]]
+[[isarray_license]]
 ----
-Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+(MIT)
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+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:
 
-   * 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.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
-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 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-2017]]
-Polymer-2017
+[[page]]
+page
 
-* @polymer/polymer
-* @webcomponents/shadycss
+* page
 
-[[Polymer-2017_license]]
+[[page_license]]
 ----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+(The MIT License)
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+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:
 
-   * 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.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
 
-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 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.
+
+----
+
+
+[[path-to-regexp]]
+path-to-regexp
+
+* path-to-regexp
+
+[[path-to-regexp_license]]
+----
+The MIT License (MIT)
+
+Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
+
+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
index 4a2630a..55ab4a0 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3190,209 +3190,15 @@
 ----
 
 
-[[ba-linkify]]
-ba-linkify
+[[Polymer-2014]]
+Polymer-2014
 
-* ba-linkify
+* @polymer/paper-ripple
+* @polymer/paper-styles
 
-[[ba-linkify_license]]
+[[Polymer-2014_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
-
-* 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.
-
-----
-
-
-[[isarray]]
-isarray
-
-* isarray
-
-[[isarray_license]]
-----
-(MIT)
-
-Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
-
-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
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation 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.
-
-----
-
-
-[[page]]
-page
-
-* page
-
-[[page_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.
-
-----
-
-
-[[path-to-regexp]]
-path-to-regexp
-
-* path-to-regexp
-
-[[path-to-regexp_license]]
-----
-The MIT License (MIT)
-
-Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
-
-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-2018]]
-Polymer-2018
-
-* @webcomponents/webcomponentsjs
-* polymer-bridges
-* polymer-resin
-
-[[Polymer-2018_license]]
-----
-Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
+Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
 
 This code may only be used under the BSD style license found at
 http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
@@ -3430,250 +3236,6 @@
 ----
 
 
-[[shadow-selection-polyfill]]
-shadow-selection-polyfill
-
-* shadow-selection-polyfill
-
-[[shadow-selection-polyfill_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.
-
-----
-
-
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-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.
-
-----
-
-
 [[Polymer-2015]]
 Polymer-2015
 
@@ -3761,6 +3323,133 @@
 ----
 
 
+[[Polymer-2017]]
+Polymer-2017
+
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
+
+[[Polymer-2017_license]]
+----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+
+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.
+
+----
+
+
+[[Polymer-2018]]
+Polymer-2018
+
+* @webcomponents/webcomponentsjs
+* polymer-bridges
+* polymer-resin
+
+[[Polymer-2018_license]]
+----
+Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
+
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+
+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.
+
+----
+
+
+[[ba-linkify]]
+ba-linkify
+
+* 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.
+
+----
+
+
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
@@ -4214,94 +3903,99 @@
 ----
 
 
-[[Polymer-2014]]
-Polymer-2014
+[[isarray]]
+isarray
 
-* @polymer/paper-ripple
-* @polymer/paper-styles
+* isarray
 
-[[Polymer-2014_license]]
+[[isarray_license]]
 ----
-Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+(MIT)
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+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:
 
-   * 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.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
-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 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-2017]]
-Polymer-2017
+[[page]]
+page
 
-* @polymer/polymer
-* @webcomponents/shadycss
+* page
 
-[[Polymer-2017_license]]
+[[page_license]]
 ----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+(The MIT License)
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+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:
 
-   * 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.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
 
-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 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.
+
+----
+
+
+[[path-to-regexp]]
+path-to-regexp
+
+* path-to-regexp
+
+[[path-to-regexp_license]]
+----
+The MIT License (MIT)
+
+Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
+
+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/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index 29bb409..e34071f 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -19,8 +19,7 @@
 
 . A Unix-based server, including any Linux flavor, MacOS, or Berkeley Software
     Distribution (BSD).
-. Java SE Runtime Environment version 1.8. Gerrit is not compatible with Java
-    9 or newer yet.
+. Java SE Runtime Environment version 11 and up.
 
 == Download Gerrit
 
diff --git a/Documentation/logs.txt b/Documentation/logs.txt
index a95956c..f072984 100644
--- a/Documentation/logs.txt
+++ b/Documentation/logs.txt
@@ -8,6 +8,12 @@
 at server startup and then daily at 11pm and
 link:config-gerrit.html#log.rotate[rotated] every midnight.
 
+== Time format
+
+For all timestamps the format `[yyyy-MM-dd'T'HH:mm:ss,SSSXXX]` is used.
+This format is both link:https://www.w3.org/TR/NOTE-datetime[ISO 8601] and
+link:https://tools.ietf.org/html/rfc3339[RFC3339] compatible.
+
 == Logs
 
 The following logs can be written.
@@ -32,10 +38,7 @@
 * `username`: the username used by the client for authentication. "-" for
   anonymous requests.
 * `[date:time]`: The date and time stamp of the HTTP request.
-  The time that the request was received. The format is until Gerrit 3.1
-  `[dd/MMM/yyyy:HH:mm:ss.SSS ZZZZ]`. For Gerrit 3.2 or newer
-  link:https://www.w3.org/TR/NOTE-datetime[ISO 8601 format] `[yyyy-MM-dd'T'HH:mm:ss,SSSZ]`
-  is used for all timestamps.
+  The time that the request was received.
 * `request`: The request line from the client is given in double quotes.
 ** the HTTP method used by the client.
 ** the resource the client requested.
@@ -68,10 +71,6 @@
 Log format:
 
 * `[date time]`: The time that the request was received.
-  The format is until Gerrit 3.1 `[yyyy-mm-dd HH:mm:ss.SSS ZZZZ]`.
-  For Gerrit 3.2 or newer
-  link:https://www.w3.org/TR/NOTE-datetime[ISO 8601 format] `[yyyy-MM-dd'T'HH:mm:ss,SSSZ]`
-  is used for all timestamps.
 * `sessionid`: hexadecimal session identifier, all requests of the
   same connection share the same sessionid. Gerrit does not support multiplexing multiple
   sessions on the same connection. Grep the log file using the sessionid as filter to
@@ -140,10 +139,6 @@
 Log format:
 
 * `[date time]`: The time that the request was received.
-  The format is until Gerrit 3.1 `[yyyy-mm-dd HH:mm:ss.SSS ZZZZ]`.
-  For Gerrit 3.2 or newer
-  link:https://www.w3.org/TR/NOTE-datetime[ISO 8601 format] `[yyyy-MM-dd'T'HH:mm:ss,SSSZ]`
-  is used for all timestamps.
 * `[thread name]`: : name of the Java thread executing the request.
 * `level`: log level (ERROR, WARN, INFO, DEBUG).
 * `logger`: name of the logger.
@@ -158,10 +153,6 @@
 Log format:
 
 * `[date time]`: The time that the request was received.
-  The format is until Gerrit 3.1 `[yyyy-mm-dd HH:mm:ss.SSS ZZZZ]`.
-  For Gerrit 3.2 or newer
-  link:https://www.w3.org/TR/NOTE-datetime[ISO 8601 format] `[yyyy-MM-dd'T'HH:mm:ss,SSSZ]`
-  is used for all timestamps.
 * `level`: log level (ERROR, WARN, INFO, DEBUG).
 * `message`: log message.
 
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 5eadc74..74ebe144 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -64,6 +64,7 @@
 * `caches/memory_eviction_count`: Memory eviction count.
 * `caches/disk_cached`: Disk entries used by persistent cache.
 * `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
+* `caches/refresh_count`: The number of refreshes per cache with an indicator if a reload was necessary.
 
 Cache disk metrics are expensive to compute on larger installations and are not
 computed by default. They can be enabled via the
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index a13cbfb..7b436a9 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -192,5 +192,5 @@
 
 In case of rollback from NoteDB to ReviewDB, all the meta refs and the
 sequence ref need to be removed.
-The [remove-notedb-refs.sh,role=external,window=_blank](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh)
+The link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh[remove-notedb-refs.sh,role=external,window=_blank]
 script has been written to automate this process.
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 1ce1d61..91bc476 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -11,34 +11,27 @@
 [[loading]]
 == Plugin loading and initialization
 
-link:js-api.html#_entry_point[Entry point] for the plugin and the loading method
-is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports,role=external,window=_blank]
-spec.
+link:js-api.html#_entry_point[Entry point] for the plugin.
 
-* The plugin provides pluginname.html, and can be a standalone file or a static
+* The plugin provides pluginname.js, and can be a standalone file or a static
   asset in a jar as a link:dev-plugins.html#deployment[Web UI plugin].
-* pluginname.html contains a `dom-module` tag with a script that uses
-  `Gerrit.install()`. There should only be single `Gerrit.install()` per file.
-* PolyGerrit imports pluginname.html along with all required resources defined in it
-  (fonts, styles, etc).
-* For standalone plugins, the entry point file is a `pluginname.html` file
+* pluginname.js contains a call to `Gerrit.install()`. There should
+  only be single `Gerrit.install()` per file.
+* PolyGerrit imports pluginname.js.
+* For standalone plugins, the entry point file is a `pluginname.js` file
   located in `gerrit-site/plugins` folder, where `pluginname` is an alphanumeric
   plugin name.
 
 Note: Code examples target modern browsers (Chrome, Firefox, Safari, Edge).
 
-Here's a recommended starter `myplugin.html`:
+Here's a recommended starter `myplugin.js`:
 
-``` html
-<dom-module id="my-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      'use strict';
+``` js
+Gerrit.install(plugin => {
+  'use strict';
 
-      // Your code here.
-    });
-  </script>
-</dom-module>
+  // Your code here.
+});
 ```
 
 [[low-level-api-concepts]]
@@ -103,34 +96,31 @@
 === Styling DOM Elements
 
 A plugin may provide Polymer's
-https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[style
+https://polymer-library.polymer-project.org/3.0/docs/devguide/style-shadow-dom[style
 modules,role=external,window=_blank] to style individual endpoints using
 `plugin.registerStyleModule(endpointName, moduleName)`. A style must be defined
-as a standalone `<dom-module>` defined in the same .html file.
+as a standalone `<dom-module>` defined in the same .js file.
+
+See `samples/theme-plugin.js` for examples.
 
 Note: TODO: Insert link to the full styling API.
 
-``` html
-<dom-module id="my-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.registerStyleModule('change-metadata', 'some-style-module');
-    });
-  </script>
-</dom-module>
-
-<dom-module id="some-style-module">
-  <style>
+``` js
+const styleElement = document.createElement('dom-module');
+styleElement.innerHTML =
+ `<template>
+    <style>
     html {
-      --change-metadata-label-status: {
-        display: none;
-      }
-      --change-metadata-strategy: {
-        display: none;
-      }
+      --primary-text-color: red;
     }
-  </style>
-</dom-module>
+   </style>
+ </template>`;
+
+styleElement.register('some-style-module');
+
+Gerrit.install(plugin => {
+  plugin.registerStyleModule('change-metadata', 'some-style-module');
+});
 ```
 
 [[high-level-api-concepts]]
@@ -152,11 +142,11 @@
 `plugin.attributeHelper(element)`
 
 Alternative for
-link:https://www.polymer-project.org/1.0/docs/devguide/data-binding[Polymer data
+link:https://polymer-library.polymer-project.org/3.0/docs/devguide/data-binding[Polymer data
 binding,role=external,window=_blank] for plugins that don't use Polymer. Can be used to bind element
 attribute changes to callbacks.
 
-See `samples/bind-parameters.html` for examples on both Polymer data bindings
+See `samples/bind-parameters.js` for examples on both Polymer data bindings
 and `attibuteHelper` usage.
 
 === eventHelper
@@ -253,26 +243,29 @@
 
 Here's the recommended approach that uses Polymer for generating custom elements:
 
-``` html
-<dom-module id="some-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.registerCustomComponent(
-        'change-view-integration', 'some-ci-module');
-    });
-  </script>
-</dom-module>
+``` js
+class SomeCiModule extends Polymer.Element {
+  static get is() {
+    return "some-ci-module";
+  }
+  static get template() {
+    return Polymer.html`
+      Sample link: <a href="http://some.com/foo">Foo</a>
+    `;
+  }
+}
 
-<dom-module id="some-ci-module">
-  <template>
-    Sample link: <a href="http://some.com/foo">Foo</a>
-  </template>
-  <script>
-    Polymer({is: 'some-ci-module'});
-  </script>
-</dom-module>
+// Register this element
+customElements.define(SomeCiModule.is, SomeCiModule);
+
+// Install the plugin
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent('change-view-integration', 'some-ci-module');
+});
 ```
 
+See `samples/` for more examples.
+
 Here's a minimal example that uses low-level DOM Hooks API for the same purpose:
 
 ``` js
@@ -382,93 +375,4 @@
 Note: TODO
 
 === url
-`plugin.url(opt_path)`
-
-Note: TODO
-
-[[deprecated-api]]
-== Deprecated APIs
-
-Some of the deprecated APIs have limited implementation in PolyGerrit to serve
-as a "stepping stone" to allow gradual migration.
-
-=== install
-`plugin.deprecated.install()`
-
-.Params:
-- none
-
-Replaces plugin APIs with a deprecated version. This allows use of deprecated
-APIs without changing JS code. For example, `onAction` is not available by
-default, and after `plugin.deprecated.install()` it's accessible via
-`self.onAction()`.
-
-=== onAction
-`plugin.deprecated.onAction(type, view_name, callback)`
-
-.Params:
-- `*string* type` Action type.
-- `*string* view_name` REST API action.
-- `*function(actionContext)* callback` Callback invoked on action button click.
-
-Adds a button to the UI with a click callback. Exact button location depends on
-parameters. Callback is triggered with an instance of
-link:#deprecated-action-context[action context].
-
-Support is limited:
-
-- type is either `change` or `revision`.
-
-See link:js-api.html#self_onAction[self.onAction] for more info.
-
-=== panel
-`plugin.deprecated.panel(extensionpoint, callback)`
-
-.Params:
-- `*string* extensionpoint`
-- `*function(screenContext)* callback`
-
-Adds a UI DOM element and triggers a callback with context to allow direct DOM
-access.
-
-Support is limited:
-
-- extensionpoint is one of the following:
- * CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK
- * CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK
-
-See link:js-api.html#self_panel[self.panel] for more info.
-
-=== settingsScreen
-`plugin.deprecated.settingsScreent(path, menu, callback)`
-
-.Params:
-- `*string* path` URL path fragment of the screen for direct link.
-- `*string* menu` Menu item title.
-- `*function(settingsScreenContext)* callback`
-
-Adds a settings menu item and a section in the settings screen that is provided
-to plugin for setup.
-
-See link:js-api.html#self_settingsScreen[self.settingsScreen] for more info.
-
-[[deprecated-action-context]]
-=== Action Context (deprecated)
-Instance of Action Context is passed to `onAction()` callback.
-
-Support is limited:
-
-- `popup()`
-- `hide()`
-- `refresh()`
-- `textfield()`
-- `br()`
-- `msg()`
-- `div()`
-- `button()`
-- `checkbox()`
-- `label()`
-- `prependLabel()`
-- `call()`
-
-See link:js-api.html#ActionContext[Action Context] for more info.
+`plugin.url(opt_path)`
\ No newline at end of file
diff --git a/Documentation/pg-plugin-migration.txt b/Documentation/pg-plugin-migration.txt
index bca4b7a..061c687 100644
--- a/Documentation/pg-plugin-migration.txt
+++ b/Documentation/pg-plugin-migration.txt
@@ -79,9 +79,6 @@
   <script>
     Gerrit.install(plugin => {
         // Setup block, is executed before sampleplugin.js
-
-        // Install deprecated JS APIs (onAction, popup, etc)
-        plugin.deprecated.install();
     });
   </script>
 
@@ -105,8 +102,6 @@
 - `sampleplugin.js` is loaded since it's referenced in `sampleplugin.html`
 - setup script tag code is executed before `sampleplugin.js`
 - cleanup script tag code is executed after `sampleplugin.js`
-- `plugin.deprecated.install()` enables deprecated APIs (onAction(), popup(),
-etc) before `sampleplugin.js` is loaded
 
 This means the plugin instance is shared between .html-based and .js-based
 code. This allows to gradually and incrementally transfer code to the new API.
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
index ac69616..e5b4140 100644
--- a/Documentation/prolog-change-facts.txt
+++ b/Documentation/prolog-change-facts.txt
@@ -54,6 +54,17 @@
 |`commit_stats/3`   |`commit_stats(5,20,50).`
     |Number of files modified, number of insertions and the number of deletions.
 
+|`files/3` |`files(file('modules/jgit', 'A', 'SUBMODULE')).`
+
+           |`files(file('a.txt', 'M', 'REGULAR')).'
+
+    A list of tuples: The first argument is a file name of the current patchset.
+    The second argument is the modification type of this file, with the options being
+    'A' for 'added', 'M' for 'modified', 'D' for 'deleted', 'R' for 'renamed', 'C' for
+    'COPIED' and 'W' for 'rewrite'.
+    The third argument is the type of file, with the options being a submodule file
+    'SUBMODULE' and a non-submodule file being 'REGULAR'.
+
 |`pure_revert/1`     |`pure_revert(1).`
     |link:rest-api-changes.html#get-pure-revert[Pure revert] as integer atom (1 if
         the change is a pure revert, 0 otherwise)
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 21b8c9f..32c30b8 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -951,40 +951,40 @@
 sum_list([], S, S).
 ----
 
-=== Example 14: Master and apprentice
-The master and apprentice example allow you to specify a user (the `master`)
-that must approve all changes done by another user (the `apprentice`).
+=== Example 14: Mentor and Mentee
+The mentor and mentee example allow you to specify a user (the `mentor`)
+that must approve all changes done by another user (the `mentee`).
 
 The code first checks if the commit author is in the apprentice database.
-If the commit is done by an `apprentice`, it will check if there is a `+2`
-review by the associated `master`.
+If the commit is done by a `mentee`, it will check if there is a `+2`
+review by the associated `mentor`.
 
 `rules.pl`
 [source,prolog]
 ----
-% master_apprentice(Master, Apprentice).
-% Extend this with appropriate user-id for your master/apprentice setup.
-master_apprentice(user(1000064), user(1000000)).
+% mentor_mentee(Mentor, Mentee).
+% Extend this with appropriate user-id for your mentor/mentee setup.
+mentor_mentee(user(1000064), user(1000000)).
 
 submit_rule(S) :-
     gerrit:default_submit(In),
     In =.. [submit | Ls],
-    add_apprentice_master(Ls, R),
+    add_mentee_mentor(Ls, R),
     S =.. [submit | R].
 
-check_master_approval(S1, S2, Master) :-
+check_mentor_approval(S1, S2, Mentor) :-
     gerrit:commit_label(label('Code-Review', 2), R),
-    R = Master, !,
-    S2 = [label('Master-Approval', ok(R)) | S1].
-check_master_approval(S1, [label('Master-Approval', need(_)) | S1], _).
+    R = Mentor, !,
+    S2 = [label('Mentor-Approval', ok(R)) | S1].
+check_mentor_approval(S1, [label('Mentor-Approval', need(_)) | S1], _).
 
-add_apprentice_master(S1, S2) :-
+add_mentee_mentor(S1, S2) :-
     gerrit:commit_author(Id),
-    master_apprentice(Master, Id),
+    mentor_mentee(Mentor, Id),
     !,
-    check_master_approval(S1, S2, Master).
+    check_mentor_approval(S1, S2, Mentor).
 
-add_apprentice_master(S, S).
+add_mentee_mentor(S, S).
 ----
 
 === Example 15: Make change submittable if all comments have been resolved
@@ -1083,6 +1083,35 @@
 indicate to the user that the change has to be a pure revert in order
 to become submittable.
 
+=== Example 17: Make a change submittable if it doesn't include specific files
+
+We can block any change which contains a submodule file change:
+
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(R)) :-
+  gerrit:includes_file(file(_,_,'SUBMODULE')),
+  !,
+  R = label('All-Submodules-Resolved', need(_)).
+submit_rule(submit(label('All-Submodules-Resolved', ok(A)))) :-
+  gerrit:commit_author(A).
+----
+
+We can also block specific files, modification type, or file type,
+by changing include_files/1 to a different parameter. E.g,
+include_files('a.txt',_,_) includes any update to "a.txt", and
+('a.txt','D',_) includes any deletion to "a.txt". Also, (_,_,_) includes
+any file (other than magic file).
+
+An inclusive list of possible arguments using the code above with variations
+of include_file:
+The first parameter is the file name.
+The second is the modification type ('A' for 'added', 'M' for 'modified',
+'D' for 'deleted', 'R' for 'renamed', 'C' for 'COPIED' and 'W' for 'rewrite').
+The third argument is the type of file, with the options being a submodule
+file 'SUBMODULE' and a non-submodule file being 'REGULAR'.
+
 == Examples - Submit Type
 The following examples show how to implement own submit type rules.
 
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index c2a7d21..6664aa2 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -287,12 +287,12 @@
          "15bfcd8a6de1a69c50b30cedcdcc951c15703152": {
            "url": "#/admin/groups/uuid-15bfcd8a6de1a69c50b30cedcdcc951c15703152",
            "options": {},
-           "description": "Users who perform batch actions on Gerrit",
+           "description": "Service accounts that interact with Gerrit",
            "group_id": 2,
            "owner": "Administrators",
            "owner_id": "53a4f647a89ea57992571187d8025f830625192a",
            "created_on": "2009-06-08 23:31:00.000000000",
-           "name": "Non-Interactive Users"
+           "name": "Service Users"
          },
          "global:Anonymous-Users": {
            "options": {},
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 6fbedb0..2a59d0c 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -60,7 +60,7 @@
 [[details]]
 --
 * `DETAILS`: Includes full name, preferred email, username, display
-name, avatars, status and state for each account.
+name, avatars, status, state and tags for each account.
 --
 
 [[all-emails]]
@@ -1284,6 +1284,7 @@
   )]}'
   {
     "changes_per_page": 25,
+    "theme": "LIGHT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "diff_view": "SIDE_BY_SIDE",
@@ -1336,6 +1337,7 @@
 
   {
     "changes_per_page": 50,
+    "theme": "DARK",
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
@@ -1383,6 +1385,7 @@
   )]}'
   {
     "changes_per_page": 50,
+    "theme" "DARK",
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
@@ -2299,6 +2302,12 @@
 |`status`          |optional|Status message of the account.
 |`inactive`        |not set if `false`|
 Whether the account is inactive.
+|`tags`      |optional, not set if empty|
+List of additional tags that this account has. The only +
+current tag an account can have is `SERVICE_USER`. +
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS]
 |===============================
 
 [[account-input]]
@@ -2759,6 +2768,9 @@
 |`changes_per_page`             ||
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
+|`theme`                        ||
+Which theme to use.
+Allowed values are `DARK` or `LIGHT`.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
@@ -2795,9 +2807,11 @@
 |`email_strategy`               ||
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
-their own comments. On `DISABLED` the user will not receive any email
-notifications from Gerrit.
-Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
+their own comments. On `ATTENTION_SET_ONLY`, on emails about changes, the user
+will receive emails only if they are in the attention set of that change.
+On `DISABLED` the user will not receive any email notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `ATTENTION_SET_ONLY`,
+`DISABLED`.
 |`default_base_for_merges`      ||
 The base which should be pre-selected in the 'Diff Against' drop-down
 list when the change screen is opened for a merge commit.
@@ -2821,6 +2835,9 @@
 |`changes_per_page`             |optional|
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
+|`theme`                        |optional|
+Which theme to use.
+Allowed values are `DARK` or `LIGHT`.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
@@ -2855,9 +2872,11 @@
 |`email_strategy`               |optional|
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
-their own comments. On `DISABLED` the user will not receive any email
-notifications from Gerrit.
-Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
+their own comments. On `ATTENTION_SET_ONLY`, on emails about changes, the user
+will receive emails only if they are in the attention set of that change.
+On `DISABLED` the user will not receive any email notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `ATTENTION_SET_ONLY`,
+`DISABLED`.
 |`default_base_for_merges`      |optional|
 The base which should be pre-selected in the 'Diff Against' drop-down
 list when the change screen is opened for a merge commit.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e9a4caf..32bfc6b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -74,8 +74,8 @@
 link:user-search.html#_search_operators[query string] must be provided
 by the `q` parameter. The `n` parameter can be used to limit the
 returned results. The `no-limit` parameter can be used remove the default
-limit on queries and return all results. This might not be supported by
-all index backends.
+limit on queries and return all results (does not apply to anonymous requests).
+This might not be supported by all index backends.
 
 As result a list of link:#change-info[ChangeInfo] entries is returned.
 The change output is sorted by the last update time, most recently
@@ -100,6 +100,15 @@
       "id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
       "project": "demo",
       "branch": "master",
+      "attention_set": [
+        {
+          "account": {
+            "name": "John Doe"
+          },
+         "last_update": "2012-07-17 07:19:27.766000000",
+         "reason": "reviewer or cc replied"
+        }
+      ]
       "change_id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
       "subject": "One change",
       "status": "NEW",
@@ -213,15 +222,18 @@
 [[labels]]
 --
 * `LABELS`: a summary of each label required for submit, and
-  approvers that have granted (or rejected) with that label.
+  approvers that have granted (or rejected) with that label
+  as well as all reviewers by state, and reviewers that may
+  be removed by the current user.
 --
 
 [[detailed-labels]]
 --
 * `DETAILED_LABELS`: detailed label information, including numeric
   values of all existing approvals, recognized label values, values
-  permitted to be set by the current user, all reviewers by state, and
-  reviewers that may be removed by the current user.
+  permitted to be set by any reviewer and the change owner, all
+  reviewers by state, and reviewers that may be removed by the
+  current user.
 --
 
 [[current-revision]]
@@ -525,6 +537,15 @@
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
+    "attention_set": [
+      {
+        "account": {
+          "name": "John Doe"
+        },
+       "last_update": "2013-02-21 11:16:36.775000000",
+       "reason": "reviewer or cc replied"
+      }
+    ]
     "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
     "subject": "Implementing Feature X",
     "status": "NEW",
@@ -577,6 +598,18 @@
     "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
     "project": "myProject",
     "branch": "master",
+    "attention_set": [
+      {
+        "account": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "username": "jdoe"
+        },
+       "last_update": "2013-02-21 11:16:36.775000000",
+       "reason": "reviewer or cc replied"
+      }
+    ]
     "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
     "subject": "Implementing Feature X",
     "status": "NEW",
@@ -1132,6 +1165,8 @@
 The request body does not need to include a link:#abandon-input[
 AbandonInput] entity if no review comment is added.
 
+Abandoning a change also removes all users from the link:#attention-set[attention set].
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/abandon HTTP/1.0
@@ -1620,6 +1655,8 @@
 The request body only needs to include a link:#submit-input[
 SubmitInput] entity if submitting on behalf of another user.
 
+Submitting a change also removes all users from the link:#attention-set[attention set].
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submit HTTP/1.0
@@ -2011,6 +2048,10 @@
 comments for each path are sorted by patch set number. Each comment has
 the `patch_set` and `author` fields set.
 
+If the `enable_context` request parameter is set to true, the comment entries
+will contain a list of link:#context-line[ContextLine] containing the lines of
+the source file where the comment was written.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/comments HTTP/1.0
@@ -2278,6 +2319,9 @@
 is added. Actions that create a new patch set in a WIP change default to
 notifying *OWNER* instead of *ALL*.
 
+Marking a change work in progress also removes all users from the
+link:#attention-set[attention set].
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/wip HTTP/1.0
@@ -2306,6 +2350,9 @@
 to include a link:#work-in-progress-input[WorkInProgressInput] entity
 if no review comment is added.
 
+Marking a change ready for review also adds all of the reviewers of the change
+to the link:#attention-set[attention set].
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/ready HTTP/1.0
@@ -3220,6 +3267,10 @@
 a CC on the change is added as reviewer, the reviewer state of that
 user is updated to reviewer.
 
+Adding a new reviewer also adds that reviewer to the attention set, unless
+the change is work in progress.
+Also, moving a reviewer to CC removes that user from the attention set.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
@@ -3361,6 +3412,7 @@
 --
 
 Deletes a reviewer from a change.
+Deleting a reviewer also removes that user from the attention set.
 
 .Request
 ----
@@ -3919,6 +3971,33 @@
 added as a reviewer, otherwise (if they only commented) they are added to
 the CC list.
 
+Some updates to the attention set occur here. If more than one update should
+occur, only the first update in the order of the below documentation occurs:
+
+If a user is part of remove_from_attention_set, the user will be explicitly
+removed from the attention set.
+
+If a user is part of add_to_attention_set, the user will be explicitly
+added to the attention set.
+
+If the boolean ignore_default_attention_set_rules is set to true, all
+other rules below will be ignored:
+
+The user who created the review is removed from the attention set.
+
+If the change is ready for review, the following also apply:
+
+When the uploader replies, the owner is added to the attention set.
+
+When the owner or uploader replies, all the reviewers are added to
+the attention set.
+
+When neither the owner nor the uploader replies, add the owner and the
+uploader to the attention set.
+
+Then, new reviewers are added to the attention set, and removed reviewers
+(by becoming CC) are removed from the attention set.
+
 A review cannot be set on a change edit. Trying to post a review for a
 change edit fails with `409 Conflict`.
 
@@ -4960,6 +5039,137 @@
   }
 ----
 
+[[get-ported-comments]]
+=== Get Ported Comments
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/ported_comments'
+--
+
+Ports comments of other revisions to the requested revision.
+
+Only comments added on earlier patchsets are ported. That set of comments is filtered even further
+due to some additional rules. Callers of this endpoint shouldn't rely on the exact logic of which
+comments are ported as that logic might change in the future. Instead, callers must be able to
+handle any smaller/larger set of comments returned by this endpoint.
+
+Typically, a comment thread is returned fully or excluded fully. However, draft comments and
+robot comments are ignored and not returned via this endpoint. Hence, it's possible to get ported
+comments from this endpoint which are a reply to a non-ported robot comment. Callers must be
+able to deal with this situation.
+
+The returned comments are organized in a map of file path to link:#comment-info[CommentInfo] entries
+in the same fashion as for the link:#list-comments[List Revision Comments] endpoint.
+The map is filled with the original comment attributes except for these attributes: `path`, `line`,
+and `range` point to the computed position in the target revision. If the exactly correct position
+can't be determined, those fields will be filled with the next best position. That can also mean
+not filling the `line` or `range` attribute anymore and thus converting the comment to a file
+comment (or even moving the comment to a different file or the patchset level). Callers of this
+endpoint must be able to deal with this and not rely on the original comment position.
+
+It's possible that this endpoint returns different link:#comment-info[CommentInfo] entries with
+the same comment UUID. This is not a bug but a feature. If a comment appears on a file which Gerrit
+recognizes as copied between patchsets, the ported version of this comment consists of two ported
+instances having the same UUID but different `file`/`line`/`range` positions. Callers must be able
+to handle this situation.
+
+Repeated calls of this endpoint might produce different results. Internal errors during the
+position computation are mapped to fallback locations for affected comments. Those errors might
+have vanished on later calls, upon which this endpoint returns the actually mapped position. In
+addition, comments can be deleted and draft comments can be published, upon which the set of ported
+comments may change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/4/ported_comments/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "id": "TvcXrmjM",
+        "patch_set": 2,
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000",
+        "author": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        },
+        "unresolved": true
+      },
+      {
+        "id": "TveXwFiA",
+        "patch_set": 2,
+        "line": 23,
+        "in_reply_to": "TvcXrmjM",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000",
+        "author": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        },
+        "unresolved": true
+      }
+    ]
+  }
+----
+
+[[get-ported-drafts]]
+=== Get Ported Drafts
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/ported_drafts'
+--
+
+Ports draft comments of other revisions to the requested revision.
+
+This endpoint behaves similarly to the link:#get-ported-comments[Get Ported Comments] endpoint.
+With this endpoint, only draft comments of the calling user are ported, though. If a draft comment
+is a reply to a published comment, only the ported draft comment is returned.
+
+Depending on the filtering rules, it's possible that this endpoint returns a draft comment which is
+a reply to a comment thread which is not returned by the
+link:#get-ported-comments[Get Ported Comments] endpoint. That's intended behavior. Callers must be
+able to handle this situation. The same holds for drafts which are a reply to a robot comment.
+
+Different than the link:#get-ported-comments[Get Ported Comments] endpoint, the `author` of the
+returned comments is not filled for this endpoint as only comments of the calling user are returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/ported_drafts/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "id": "TveXwFiA",
+        "patch_set": 2,
+        "line": 23,
+        "in_reply_to": "TvcXrmjM",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000",
+        "unresolved": true
+      }
+    ]
+  }
+----
+
 [[apply-fix]]
 === Apply Fix
 --
@@ -5384,9 +5594,6 @@
 differences are reported in the result.  Valid values are `IGNORE_NONE`,
 `IGNORE_TRAILING`, `IGNORE_LEADING_AND_TRAILING` or `IGNORE_ALL`.
 
-The `context` parameter can be specified to control the number of lines of surrounding context
-in the diff.  Valid values are `ALL` or number of lines.
-
 [[preview-fix]]
 === Preview fix
 --
@@ -5676,6 +5883,172 @@
   HTTP/1.1 204 No Content
 ----
 
+[[attention-set-endpoints]]
+== Attention Set Endpoints
+
+[[get-attention-set]]
+=== Get Attention Set
+--
+'GET /changes/link:#change-id[\{change-id\}]/attention'
+--
+
+Returns all users that are currently in the attention set.
+As response a list of link:#attention-set-info[AttentionSetInfo]
+entity is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "account": {
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com",
+        "username": "jdoe"
+      },
+      "last_update": "2013-02-01 09:59:32.126000000",
+      "reason": "reviewer or cc replied"
+    },
+    {
+      "account": {
+        "_account_id": 1000097,
+        "name": "Jane Doe",
+        "email": "jane.doe@example.com",
+        "username": "janedoe"
+      },
+      "last_update": "2013-02-01 09:59:32.126000000",
+      "reason": "Reviewer was added"
+    }
+  ]
+----
+
+[[add-to-attention-set]]
+=== Add To Attention Set
+--
+'POST /changes/link:#change-id[\{change-id\}]/attention'
+--
+
+Adds a single user to the attention set of a change.
+
+A user can only be added if they are not in the attention set.
+If a user is added while already in the attention set, the
+request is silently ignored.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention HTTP/1.0
+----
+
+Details should be provided in the request body as an
+link:#attention-set-input[AttentionSetInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "user": "John Doe",
+    "reason": "reason"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "_account_id": 1000096,
+    "name": "John Doe",
+    "email": "john.doe@example.com",
+    "username": "jdoe"
+  }
+----
+
+[[remove-from-attention-set]]
+=== Remove from Attention Set
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/attention/link:rest-api-accounts.html#account-id[\{account-id\}]' +
+'POST /changes/link:#change-id[\{change-id\}]/attention/link:rest-api-accounts.html#account-id[\{account-id\}]/delete'
+--
+
+Deletes a single user from the attention set of a change.
+
+A user can only be removed from the attention set if they
+are currently in the attention set. Otherwise, the request
+is silently ignored.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention/John%20Doe HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention/John%20Doe/delete HTTP/1.0
+----
+
+Reason can be provided in the request body as an
+link:#attention-set-input[AttentionSetInput] entity.
+
+User must be left empty, or the user must be exactly
+the same user as in the request header.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention/John%20Doe/delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "reason": "reason"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[attention-set]]
+== Attention Set
+Attention Set is the set of users that should perform some action on the
+change. E.g, reviewers should review the change, owner/uploader should
+add a new patchset or respond to comments.
+
+Users are added to the attention set if one the following apply:
+
+* They are manually added in link:#review-input[ReviewInput] in
+ add_to_attention_set.
+* They are added as reviewers.
+* The change is marked ready for review.
+* As an owner/uploader, when someone replies on your change.
+* As a reviewer, when the owner/uploader replies.
+
+Users are removed from the attention set if one the following apply:
+
+* They are manually removed in link:#review-input[ReviewInput] in
+ remove_from_attention_set.
+* They are removed from reviewers.
+* The change is marked work in progress, abandoned, or submitted.
+* When the user replies on a change.
+
+If the ignore_default_attention_set_rules in link:#review-input[ReviewInput]
+is set to true, no other changes to the attention set will occur during the
+link:#set-review[set-review].
+Also, users specified in the list will occur instead of any of the implicit
+changes to the attention set. E.g, if a user is added by add_to_attention_set
+in link:#review-input[ReviewInput], but also the change is marked work in
+progress, the user will still be added.
+
 [[ids]]
 == IDs
 
@@ -5730,6 +6103,11 @@
 The list of commits that are being integrated into the destination
 branch by submitting the merge commit.
 
+* `/PATCHSET_LEVEL`:
++
+This file path is used exclusively for posting and indicating
+patchset-level comments, thus not relevant for other use-cases.
+
 [[fix-id]]
 === \{fix-id\}
 UUID of a suggested fix.
@@ -5767,7 +6145,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[action-info]]
@@ -5868,6 +6247,42 @@
 should be added as assignee.
 |===========================
 
+[[attention-set-info]]
+=== AttentionSetInfo
+The `AttentionSetInfo` entity contains details of users that are in
+the link:#attention-set[attention set].
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`account`     || link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`last_update` || The link:rest-api.html#timestamp[timestamp] of the last update.
+|`reason`      || The reason of for adding or removing the user.
+
+|===========================
+[[attention-set-input]]
+=== AttentionSetInput
+The `AttentionSetInput` entity contains details for adding users to the
+link:#attention-set[attention set] and removing them from it.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name        ||Description
+|`user`            |optional| link:rest-api-accounts.html#account-id[ID]
+of the account that should be added to the attention set. For removals,
+this field should be empty or the same as the field in the request header.
+|`reason`          || The reason of for adding or removing the user.
+|`notify`          |optional|
+Notify handling that defines to whom email notifications should be sent
+after the change is created. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `OWNER`.
+|`notify_details`  |optional|
+Additional information about whom to notify about the change creation
+as a map of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
+|===========================
+
 [[blame-info]]
 === BlameInfo
 The `BlameInfo` entity stores the commit metadata with the row coordinates where
@@ -5924,6 +6339,9 @@
 The name of the target branch. +
 The `refs/heads/` prefix is omitted.
 |`topic`              |optional|The topic to which this change belongs.
+|`attention_set`      |optional|
+The map that maps link:rest-api-accounts.html#account-id[account IDs]
+to link:#attention-set-info[AttentionSetInfo] of that account.
 |`assignee`           |optional|
 The assignee of the change as an link:rest-api-accounts.html#account-info[
 AccountInfo] entity.
@@ -5999,7 +6417,8 @@
 |`removable_reviewers`|optional|
 The reviewers that can be removed by the calling user as a list of
 link:rest-api-accounts.html#account-info[AccountInfo] entities. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+Only set if link:#labels[labels] or
+link:#detailed-labels[detailed labels] are requested.
 |`reviewers`          |optional|
 The reviewers as a map that maps a reviewer state to a list of
 link:rest-api-accounts.html#account-info[AccountInfo] entities.
@@ -6008,13 +6427,15 @@
 `CC`: Users that were added to the change, but have not voted. +
 `REMOVED`: Users that were previously reviewers on the change, but have
 been removed. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+Only set if link:#labels[labels] or
+link:#detailed-labels[detailed labels] are requested.
 |`pending_reviewers`  |optional|
 Updates to `reviewers` that have been made while the change was in the
 WIP state. Only present on WIP changes and only if there are pending
 reviewer updates to report. These are reviewers who have not yet been
 notified about being added to or removed from the change. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+Only set if link:#labels[labels] or
+link:#detailed-labels[detailed labels] are requested.
 |`reviewer_updates`|optional|
 Updates to reviewers set for the change as
 link:#review-update-info[ReviewerUpdateInfo] entities.
@@ -6131,7 +6552,8 @@
 If not set, the default is `ALL`.
 |`notify_details`     |optional|
 Additional information about whom to notify about the change creation
-as a map of recipient type to link:#notify-info[NotifyInfo] entity.
+as a map of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |==================================
 
 [[change-message-info]]
@@ -6187,7 +6609,8 @@
 If not set, the default is `ALL`.
 |`notify_details`   |optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |`keep_reviewers`   |optional, defaults to false|
 If `true`, carries reviewers and ccs over from original change to newly created one.
 |`allow_conflicts`  |optional, defaults to false|
@@ -6224,7 +6647,7 @@
 comments may be returned for multiple patch sets.
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
-The path of the file for which the inline comment was done. +
+link:#file-id[The file path] for which the inline comment was done. +
 Not set if returned in a map where the key is the file path.
 |`side`        |optional|
 The side on which the comment was added. +
@@ -6260,9 +6683,17 @@
 resolution of a comment thread is stored in the last comment in that thread
 chronologically.
 |`change_message_id` |optional|
-Available with published comments. Contains the
-link:rest-api-changes.html#change-message-info[id] of the change message
-that this comment is linked to.
+Available with the link:#list-change-comments[list change comments] endpoint.
+Contains the link:rest-api-changes.html#change-message-info[id] of the change
+message that this comment is linked to.
+|`commit_id` |optional|
+Hex commit SHA1 (40 characters string) of the commit of the patchset to which
+this comment applies.
+|`context_lines` |optional|
+A list of link:#context-line[ContextLine] containing the lines of the source
+file where the comment was written. Available only if the "enable_context"
+parameter (see link:#list-change-comments[List Change Comments]) is set.
+
 |===========================
 
 [[comment-input]]
@@ -6277,7 +6708,7 @@
 The URL encoded UUID of the comment if an existing draft comment should
 be updated.
 |`path`        |optional|
-The path of the file for which the inline comment should be added. +
+link:#file-id[The file path] for which the inline comment should be added. +
 Doesn't need to be set if contained in a map where the key is the file
 path.
 |`side`        |optional|
@@ -6334,6 +6765,18 @@
 |`end_character`     ||The character position in the end line. (0-based)
 |===========================
 
+[[context-line]]
+=== ContextLine
+The `ContextLine` entity contains the line number and line text of a single
+line of the source file content.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name          |Description
+|`line_number`       |The line number of the source line.
+|`context_line`      |String containing the line text.
+|===========================
+
 [[commit-info]]
 === CommitInfo
 The `CommitInfo` entity contains information about a commit.
@@ -6377,7 +6820,8 @@
 If not set, the default is `OWNER` for WIP changes and `ALL` otherwise.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[delete-change-message-input]]
@@ -6423,7 +6867,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[delete-vote-input]]
@@ -6444,7 +6889,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[description-input]]
@@ -6478,6 +6924,8 @@
 link:#diff-intraline-info[DiffIntralineInfo] entity.
 |`due_to_rebase`|not set if `false`|Indicates whether this entry was introduced by a
 rebase.
+|`due_to_move`|not set if `false`|Indicates whether this entry was introduced by a
+move operation.
 |`skip`         |optional|count of lines skipped on both sides when the file is
 too large to include all common lines.
 |`common`       |optional|Set to `true` if the region is common according
@@ -6684,12 +7132,13 @@
 |==========================
 |Field Name      |Description
 |`path`          |The path of the file which should be modified. Any file in
-the repository may be modified.
+the repository may be modified. The commit message can be modified via the
+magic file `/COMMIT_MSG` though only the part below the generated header of
+that magic file can be modified. References to the header lines will result in
+errors when the fix is applied.
 |`range`         |A <<comment-range,CommentRange>> indicating which content
 of the file should be replaced. Lines in the file are assumed to be separated
-by the line feed character, the carriage return character, the carriage return
-followed by the line feed character, or one of the other Unicode linebreak
-sequences supported by Java.
+by the line feed character.
 |`replacement`   |The content which should be used instead of the current one.
 |==========================
 
@@ -6888,6 +7337,13 @@
 |`merge`              ||
 The detail of the source commit for merge as a link:#merge-input[MergeInput]
 entity.
+|`author`             |optional|
+An link:rest-api-accounts.html#account-input[AccountInput] entity
+that will set the author of the commit to create. The author must be
+specified as name/email combination.
+The caller needs "Forge Author" permission when using this field.
+This field does not affect the owner of the change, which will
+continue to use the identity of the caller.
 |==================================
 
 [[move-input]]
@@ -6908,8 +7364,8 @@
 be notified about an update. These notifications are sent out even if a
 `notify` option in the request input disables normal notifications.
 `NotifyInfo` entities are normally contained in a `notify_details` map
-in the request input where the key is the recipient type. The recipient
-type can be `TO`, `CC` and `BCC`.
+in the request input where the key is the
+link:user-notify.html#recipient-types[recipient type].
 
 [options="header",cols="1,^1,5"]
 |=======================
@@ -6964,7 +7420,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[pure-revert-info]]
@@ -7100,12 +7557,17 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the revert as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |`topic`         |optional|
 Name of the topic for the revert change. If not set, the default for Revert
 endpoint is the topic of the change being reverted, and the default for the
 RevertSubmission endpoint is `revert-{submission_id}-{timestamp.now}`.
 Topic can't contain quotation marks.
+|`work_in_progress` |optional|
+When present, change is marked as Work In Progress. This will also override
+the notify value to `OWNER`. +
+If not set, the default is false.
 |=============================
 
 [[revert-submission-info]]
@@ -7160,24 +7622,24 @@
 
 [options="header",cols="1,^1,5"]
 |============================
-|Field Name               ||Description
-|`message`                |optional|
+|Field Name                             ||Description
+|`message`                              |optional|
 The message to be added as review comment.
-|`tag`                    |optional|
+|`tag`                                  |optional|
 Apply this tag to the review comment message, votes, and inline
 comments. Tags may be used by CI or other automated systems to
 distinguish them from human reviews. Votes/comments that contain `tag` with
 'autogenerated:' prefix can be filtered out in the web UI.
-|`labels`                 |optional|
+|`labels`                               |optional|
 The votes that should be added to the revision as a map that maps the
 label names to the voting values.
-|`comments`               |optional|
+|`comments`                             |optional|
 The comments that should be added as a map that maps a file path to a
 list of link:#comment-input[CommentInput] entities.
-|`robot_comments`         |optional|
+|`robot_comments`                       |optional|
 The robot comments that should be added as a map that maps a file path
 to a list of link:#robot-comment-input[RobotCommentInput] entities.
-|`drafts`                 |optional|
+|`drafts`                               |optional|
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
 input. +
@@ -7186,29 +7648,40 @@
 Only `KEEP` is allowed when used in conjunction with `on_behalf_of`. +
 If not set, the default is `KEEP`. If `on_behalf_of` is set, then no other value
 besides `KEEP` is allowed.
-|`notify`                 |optional|
+|`notify`                              |optional|
 Notify handling that defines to whom email notifications should be sent
 after the review is stored. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
 If not set, the default is `ALL`.
-|`notify_details`         |optional|
+|`notify_details`                      |optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
-|`omit_duplicate_comments`|optional|
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
+|`omit_duplicate_comments`             |optional|
 If `true`, comments with the same content at the same place will be omitted.
-|`on_behalf_of`           |optional|
+|`on_behalf_of`                        |optional|
 link:rest-api-accounts.html#account-id[\{account-id\}] the review
 should be posted on behalf of. To use this option the caller must
 have been granted `labelAs-NAME` permission for all keys of labels.
-|`reviewers`              |optional|
+|`reviewers`                           |optional|
 A list of link:rest-api-changes.html#reviewer-input[ReviewerInput]
 representing reviewers that should be added to the change.
-|`ready`                  |optional|
+|`ready`                               |optional|
 If true, and if the change is work in progress, then start review.
 It is an error for both `ready` and `work_in_progress` to be true.
-|`work_in_progress`         |optional|
+|`work_in_progress`                    |optional|
 If true, mark the change as work in progress. It is an error for both
 `ready` and `work_in_progress` to be true.
+|`add_to_attention_set`                |optional|
+list of link:#attention-set-input[AttentionSetInput] entities to add
+to the link:#attention-set[attention set].
+|`remove_from_attention_set`           |optional|
+list of link:#attention-set-input[AttentionSetInput] entities to remove
+from the link:#attention-set[attention set].
+|`ignore_automatic_attention_set_rules`|optional|
+If set to true, ignore all automatic attention set rules described in the
+link:#attention-set[attention set]. Updates in add_to_attention_set
+and remove_from_attention_set are not ignored.
 |============================
 
 [[review-result]]
@@ -7286,7 +7759,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[revision-info]]
@@ -7352,7 +7826,8 @@
 The `RobotCommentInfo` entity contains information about a robot inline
 comment.
 
-`RobotCommentInfo` has the same fields as <<comment-info,CommentInfo>>.
+`RobotCommentInfo` has the same fields as <<comment-info,CommentInfo>>
+except for the `unresolved` field which doesn't exist for robot comments.
 In addition `RobotCommentInfo` has the following fields:
 
 [options="header",cols="1,^1,5"]
@@ -7372,8 +7847,35 @@
 The `RobotCommentInput` entity contains information for creating an inline
 robot comment.
 
-`RobotCommentInput` has the same fields as
-<<robot-comment-info,RobotCommentInfo>>.
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`path`        ||
+link:#file-id[The file path] for which the inline comment should be added.
+|`side`        |optional|
+The side on which the comment should be added. +
+Allowed values are `REVISION` and `PARENT`. +
+If not set, the default is `REVISION`.
+|`line`        |optional|
+The number of the line for which the comment should be added. +
+`0` if it is a file comment. +
+If neither line nor range is set, a file comment is added. +
+If range is set, this value is ignored in favor of the `end_line` of the range.
+|`range`       |optional|
+The range of the comment as a link:#comment-range[CommentRange]
+entity.
+|`in_reply_to` |optional|
+The URL encoded UUID of the comment to which this comment is a reply.
+|`message`     |optional|
+The comment message.
+|`robot_id`       ||The ID of the robot that generated this comment.
+|`robot_run_id`   ||An ID of the run of the robot.
+|`url`            |optional|URL to more information.
+|`properties`     |optional|Robot specific properties as map that maps arbitrary
+keys to values.
+|`fix_suggestions`|optional|Suggested fixes for this robot comment as a list of
+<<fix-suggestion-info,FixSuggestionInfo>> entities.
+|===========================
 
 [[rule-input]]
 === RuleInput
@@ -7433,7 +7935,8 @@
 If not set, the default is `ALL`.
 |`notify_details`|optional|
 Additional information about whom to notify about the update as a map
-of recipient type to link:#notify-info[NotifyInfo] entity.
+of link:user-notify.html#recipient-types[recipient type] to
+link:#notify-info[NotifyInfo] entity.
 |=============================
 
 [[submit-record]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index f76e0b8..a62ed47 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1701,7 +1701,7 @@
 |======================
 |Field Name|Description
 |`status`  |The status of the consistency problem. +
-Possible values are `ERROR` and `WARNING`.
+Possible values are `FATAL`, `ERROR` and `WARNING`.
 |`message` |Message describing the consistency problem.
 |======================
 
@@ -2011,7 +2011,7 @@
 UserConfigInfo] entity.
 |`default_theme`           |optional|
 URL to a default PolyGerrit UI theme plugin, if available.
-Located in `/static/gerrit-theme.html` by default.
+Located in `/static/gerrit-theme.js` by default.
 |=======================================
 
 [[sshd-info]]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 72974e2..52c505a 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -67,12 +67,12 @@
       "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
       "created_on": "2013-02-01 09:59:32.126000000"
     },
-    "Non-Interactive Users": {
+    "Service Users": {
       "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
       "url": "#/admin/groups/uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
       "options": {
       },
-      "description": "Users who perform batch actions on Gerrit",
+      "description": "Service accounts that interact with Gerrit",
       "group_id": 4,
       "owner": "Administrators",
       "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 7bb8084..84da169 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1153,7 +1153,7 @@
         "owner": "Administrators",
         "owner_id": "d5b7124af4de52924ed397913e2c3b37bf186948",
         "created_on": "2009-06-08 23:31:00.000000000",
-        "name": "Non-Interactive Users"
+        "name": "Service Users"
       },
       "global:Anonymous-Users": {
         "options": {},
@@ -1253,6 +1253,57 @@
   }
 ----
 
+[[create-change]]
+=== Create Change for review.
+
+This endpoint is functionally equivalent to
+link:rest-api-changes.html#create-change[create change in the change
+API], but it has the project name in the URL, which is easier to route
+in sharded deployments.
+
+.Request
+----
+  POST /projects/myProject/create.change HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "subject" : "Let's support 100% Gerrit workflow direct in browser",
+    "branch" : "master",
+    "topic" : "create-change-in-browser",
+    "status" : "NEW"
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that describes
+the resulting change.
+
+.Response
+----
+  HTTP/1.1 201 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "project": "myProject",
+    "branch": "master",
+    "topic": "create-change-in-browser",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "subject": "Let's support 100% Gerrit workflow direct in browser",
+    "status": "NEW",
+    "created": "2014-05-05 07:15:44.639000000",
+    "updated": "2014-05-05 07:15:44.639000000",
+    "mergeable": true,
+    "insertions": 0,
+    "deletions": 0,
+    "_number": 4711,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
 [[create-access-change]]
 === Create Access Rights Change for review.
 --
@@ -3589,7 +3640,7 @@
 |`plugin_config`                           |optional|
 Plugin configuration as map which maps the plugin name to a map of
 parameter names to link:#config-parameter-info[ConfigParameterInfo]
-entities.
+entities. Only filled for users who have read access to `refs/meta/config`.
 |`actions`                                 |optional|
 Actions the caller might be able to perform on this project. The
 information is a map of view names to
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
new file mode 100644
index 0000000..5e2906f
--- /dev/null
+++ b/Documentation/user-attention-set.txt
@@ -0,0 +1,191 @@
+= Gerrit Code Review - Attention Set
+
+Report a bug or send feedback using
+link:https://bugs.chromium.org/p/gerrit/issues/entry?template=Attention+Set[this Monorail template].
+You can also report a bug through the bug icon in the user hovercard and in the
+reply dialog.
+
+[[whose-turn]]
+== Whose turn is it?
+
+Code Review is a turn-based workflow going back and forth between the change
+owner and reviewers. For every change Gerrit maintains an "Attention Set" with
+users that are currently expected to act on the change. Both on the dashboard
+and on the change page, this is expressed by an arrow icon before a (bolded)
+user name:
+
+image::images/user-attention-set-icon.png["account chip with attention icon", align="center"]
+
+While the attention set brings clarity to the process it also comes with
+responsibilities and expectations. To provide the best outcome for all users, we
+suggest following these principles:
+
+* Reviewers are expected to respond in a timely manner when it is their turn. If
+  you don't plan to respond within ~24h, then you should either remove yourself
+  from the attention set or you should at least send a clarification message to
+  the change owner.
+* Change owners are expected to manage the attention set of their changes
+  carefully. They should make sure that reviewers are only in the attention set
+  when the owner waits for a response from them.
+
+On the plus side you can strictly ignore everyone else's changes, if you are not
+in the attention set. :-)
+
+=== Rules
+
+To help with the back and forth, Gerrit applies some basic automated rules for
+changing the attention set:
+
+* If reviewers are added to a change, then they are added to the attention set.
+  * Exception: A reviewer adding themselves along with a comment or vote.
+* If an active change is submitted, abandoned or reset to "work in progress",
+  then all users are removed from the attention set.
+* Replying (commenting, voting or just writing a change message) removes the
+  replying user from the attention set. And it adds all participants of comment
+  conversations that the user is replying to.
+* If a *reviewer* replies, then the change owner (and uploader) are added to the
+  attention set.
+* For merged and abandoned changes the owner is added only when a human creates
+  an unresolved comment.
+* Only owner, uploader, reviewers and ccs can be in the attention set.
+
+*!IMPORTANT!* These rules are not meant to be super smart and to always do the
+right thing, e.g. if the change owner sends a reply, then they are often
+expected to individually select whose turn it is.
+
+Note that just uploading a new patchset is not a relevant event for the
+attention set to change.
+
+=== Interaction
+
+There are three ways to interact with the attention set: The attention icon,
+the hovercard of owner and reviewer chips and the "Reply" dialog.
+
+*The attention icon* can be used to quickly remove yourself (or someone else)
+from the attention set. Just click the icon, and it will disappear:
+
+image::images/user-attention-set-icon-click.png["attention set icon with tooltip", align="center"]
+
+*The hovercard* (on both the Dashboard and Change page) contains information
+about whether, why and when a user was added to the attention set. It also
+contains an action for adding/removing the user to/from the attention set.
+
+image::images/user-attention-set-hovercard.png["user hovercard with info and action", align="center"]
+
+*The reply dialog* contains a section for controlling to whom the turn should be
+passed.
+
+image::images/user-attention-set-reply-modify.png["reply dialog section for modifying", align="center"]
+
+If you click "MODIFY", then the section will
+expand and you can select and de-select users by clicking on their chips.
+Whatever you select here will be the new state of the attention set for this
+change. As a change owner make sure to remove reviewers that you don't expect to
+take action.
+
+image::images/user-attention-set-reply-select.png["reply dialog section for selecting users", align="center"]
+
+=== Bots
+
+The attention set is meant for human reviews only. Triggering bots and reacting
+to their results is a different workflow and not in scope of the attenion set.
+Thus members of the "Service Users" group will never be added to the
+attention set. And replies by such users will only add the change owner (and
+uploader) to the attention set, if it comes along with a negative vote.
+
+=== Dashboard
+
+The default *dashboard* contains a new section at the top called "Your Turn". It
+lists all changes where the logged-in user is in the attention set. When you are
+a reviewer, the change is highlighted and is shown at the top of the section.
+The "Waiting" column indicates how long the owner has already been waiting for
+you to act.
+
+image::images/user-attention-set-dashboard.png["dashboard with Your Turn section", align="center"]
+
+As an active developer, one of your daily goals will be to iterate over this
+list and clear it.
+
+image::images/user-attention-set-dashboard-empty.png["dashboard with empty Your Turn section", align="center"]
+
+Note that you can also navigate to other users' dashboards to check their
+"Your Turn" section.
+
+=== Emails
+
+Every email begins with `Attention is currently required from: ...`, so you can
+identify at a glance whether you are expected to act.
+
+You can even change your email notification preferences in the user settings to
+only receive emails when you are in the attention set of a change:
+
+image::images/user-attention-set-user-prefs.png["user preference for email notifications", align="center"]
+
+If you prefer setting up customized filters in your mail client, then you can
+make use of the `Gerrit-Attention:` footer lines that are added for every user
+in the attention set, e.g.
+
+----
+Gerrit-Attention: Marian Harbach <mharbach@google.com>
+----
+
+=== Assignee
+
+While the "Assignee" feature can still be used together with the attention set,
+we do not recommend doing so. Using both features is likely confusing. The
+distinct feature of the "Assignee" compared to the attention set is that only
+one user can be the assignee at the same time. So the assignee can be used to
+single out one person or escalate, if there are multiple reviewers. Since
+*every* reviewer in the attention set is expected to take action, singling out
+is not likely to be important and also still achievable with the attention set.
+Otherwise "Assignee" and "Attention Set" are very much overlapping, so we
+recommend to only use one of them.
+
+If you don't expect action from reviewers, then consider adding them to CC
+instead.
+
+The "Assignee" feature can be turned on/off with the
+link:config-gerrit.html#change.enableAttentionSet[enableAssignee] config option.
+
+=== Bold Changes / Mark Reviewed
+
+Before the attention set feature, changes were bolded in the dashboard when
+*something* happened and you could explicitly "mark a change reviewed" on the
+change page. This former way of keeping track of what you should look at has
+been replaced by the attention set.
+
+=== For Gerrit Admins
+
+The Attention Set has been available since the 3.3 release (late 2020). It
+is enabled by default, but you can disable it by setting
+link:config-gerrit.html#change.enableAttentionSet[enableAttentionSet] to false.
+
+As part of Gerrit 3.3 upgrade, the user group "Non-Interactive Users" is
+renamed "Service Users". For a new installation, the group is automatically
+created upon initialization.
+
+=== Important note for all host owners, project owners, and bot owners
+
+If you are a host/project owner, please make sure all bots that run against your
+host/project are part of the link:access-control.html#service_users[Service Users] group.
+
+If you are a bot owner, please make sure your bot is part of the "Service Users"
+group on all hosts it runs on.
+
+To add users to the "Service Users" group, first ensure that the group exists on
+your host. If it doesn't, create it. The name must exactly be "Service Users".
+
+To create a group, use the Gerrit UI; BROWSE -> Groups -> CREATE NEW.
+
+Then, add the bots as members in this group. Alternatively, add an existing
+group that has multiple bots as a subgroup of "Service Users".
+
+To add members or subgroups, use the Gerrit UI; BROWSE -> Groups ->
+search for "Service Users" -> Members.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
index e64d625..364b0d9 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -12,7 +12,7 @@
 
 Dashboards are available via URLs like:
 ----
-  /#/dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
+  /dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
 ----
 This opens a view showing the title "Custom View" with two sections,
 "To Review" and "Pending in myproject":
@@ -51,7 +51,7 @@
 to changes for the current user:
 
 ----
-  /#/dashboard/?title=Mine&foreach=owner:self&My+Pending=is:open&My+Merged=is:merged
+  /dashboard/?title=Mine&foreach=owner:self&My+Pending=is:open&My+Merged=is:merged
 ----
 
 
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 5346b2e..5ee3136 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -6,6 +6,15 @@
 uploaded for review, after comments have been posted on a change,
 or after the change has been submitted to a branch.
 
+[[recipient-types]]
+== Recipient Type
+
+Those are the available recipient types:
++
+* `to`: The standard To field is used; addresses are visible to all.
+* `cc`: The standard CC field is used; addresses are visible to all.
+* `bcc`: SMTP RCPT TO is used to hide the address.
+
 [[user]]
 == User Level Settings
 
@@ -114,10 +123,8 @@
 Email header used to list the destination. If not set BCC is used.
 Only one value may be specified. To use different headers for each
 address list them in different notify blocks.
-+
-* `to`: The standard To field is used; addresses are visible to all.
-* `cc`: The standard CC field is used; addresses are visible to all.
-* `bcc`: SMTP RCPT TO is used to hide the address.
+
+The possible options are the link:#recipient-types[recipient types].
 
 [[notify.name.filter]]notify.<name>.filter::
 +
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 8caf656..b22788a 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -556,14 +556,17 @@
 author:'AUTHOR'::
 +
 Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
-the author's exact email address, or part of the name or email address.
+the author's exact email address, or part of the name or email address. The
+special case of `author:self` will find changes authored by the caller.
 
 [[committer]]
 committer:'COMMITTER'::
 +
 Changes where 'COMMITTER' is the committer of the current patch set.
 'COMMITTER' may be the committer's exact email address, or part of the name or
-email address.
+email address. The special case of `committer:self` will find changes committed
+by the caller.
+
 
 [[submittable]]
 submittable:'SUBMIT_STATUS'::
diff --git a/README.md b/README.md
index a76dac6..8a4379b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
 [Gerrit](https://www.gerritcodereview.com) is a code review and project
 management tool for Git based projects.
 
-[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-master/)
+[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-java11-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-java11-master/)
+![Maven Central](https://img.shields.io/maven-central/v/com.google.gerrit/gerrit-war)
 
 ## Objective
 
@@ -76,13 +77,13 @@
 
 Docker images of Gerrit are available on [DockerHub](https://hub.docker.com/u/gerritforge/)
 
-To run a CentOS 7 based Gerrit image:
+To run a CentOS 8 based Gerrit image:
 
-        docker run -p 8080:8080 gerritforge/gerrit-centos7[:version]
+        docker run -p 8080:8080 gerritcodereview/gerrit[:version]-centos8
 
-To run a Ubuntu 15.04 based Gerrit image:
+To run a Ubuntu 20.04 based Gerrit image:
 
-        docker run -p 8080:8080 gerritforge/gerrit-ubuntu15.04[:version]
+        docker run -p 8080:8080 gerritcodereview/gerrit[:version]-ubuntu20
 
 _NOTE: release is optional. Last released package of the version is installed if the release
 number is omitted._
diff --git a/WORKSPACE b/WORKSPACE
index 3906453..cdec888 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -10,6 +10,8 @@
 #    @ui_npm folder must not have devDependencies. All dev dependencies must be placed in @ui_dev_npm
 # 4. @ui_dev_npm (polygerrit-ui/node_modules) - devDependencies for polygerrit. The packages from these
 #    folder can be used for testing, but must not be included in the final bundle.
+# 5. @plugins_npm (plugins/node_modules) - plugin dependencies for polygerrit plugins.
+#    The packages here are expected to be used in plugins.
 # Note: separation between @ui_npm and @ui_dev_npm is necessary because with bazel we can't generate
 #    two managed directories from the same package.json. At the same time we want to avoid accidental
 #    usages of code from devDependencies in polygerrit bundle.
@@ -20,6 +22,7 @@
         "@ui_npm": ["polygerrit-ui/app/node_modules"],
         "@ui_dev_npm": ["polygerrit-ui/node_modules"],
         "@tools_npm": ["tools/node_tools/node_modules"],
+        "@plugins_npm": ["plugins/node_modules"],
     },
 )
 
@@ -30,73 +33,80 @@
 load("//tools:nongoogle.bzl", "declare_nongoogle_deps")
 
 http_archive(
-    name = "bazel_toolchains",
-    sha256 = "1adf7a8e9901287c644dcf9ca08dd8d67a69df94bedbd57a841490a84dc1e9ed",
-    strip_prefix = "bazel-toolchains-5.0.0",
+    name = "platforms",
+    sha256 = "379113459b0feaf6bfbb584a91874c065078aa673222846ac765f86661c27407",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/v5.0.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-toolchains/archive/v5.0.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz",
+        "https://github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz",
     ],
 )
 
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-# Creates a default toolchain config for RBE.
-# Use this as is if you are using the rbe_ubuntu16_04 container,
-# otherwise refer to RBE docs.
-rbe_autoconfig(name = "rbe_default")
-
-# TODO(davido): Switch to upstream again, when this PR is merged:
-# https://github.com/bazelbuild/rules_closure/pull/478
 http_archive(
-    name = "io_bazel_rules_closure",
-    sha256 = "b9c2bc6ba377aa497eb7c31681d34404febf9d4e3c9c7d98ce0d78238a0af20f",
-    strip_prefix = "rules_closure-0.31",
+    name = "rbe_jdk11",
+    sha256 = "766796de71916118e528b9f4334c29c9c9b4e926227bf3264dee555e6a4306c8",
+    strip_prefix = "rbe_autoconfig-2.0.0",
     urls = [
-        "https://github.com/davido/rules_closure/archive/V0.31.tar.gz",
-        "https://gerrit-ci.gerritforge.com/lib/V0.31.tar.gz",
+        "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v2.0.0.tar.gz",
+        "https://github.com/davido/rbe_autoconfig/archive/v2.0.0.tar.gz",
     ],
 )
 
 http_archive(
+    name = "com_google_protobuf",
+    sha256 = "71030a04aedf9f612d2991c1c552317038c3c5a2b578ac4745267a45e7037c29",
+    strip_prefix = "protobuf-3.12.3",
+    urls = [
+        "https://github.com/protocolbuffers/protobuf/archive/v3.12.3.tar.gz",
+    ],
+)
+
+load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
+
+protobuf_deps()
+
+http_archive(
     name = "build_bazel_rules_nodejs",
-    patch_args = ["-p1"],
-    patches = ["//:rules_nodejs-1.5.patch"],
-    sha256 = "d0c4bb8b902c1658f42eb5563809c70a06e46015d64057d25560b0eb4bdc9007",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.5.0/rules_nodejs-1.5.0.tar.gz"],
+    sha256 = "c077680a307eb88f3e62b0b662c2e9c6315319385bc8c637a861ffdbed8ca247",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.1.0/rules_nodejs-5.1.0.tar.gz"],
 )
 
-# File is specific to Polymer and copied from the Closure Github -- should be
-# synced any time there are major changes to Polymer.
-# https://github.com/google/closure-compiler/blob/master/contrib/externs/polymer-1.0.js
-http_file(
-    name = "polymer_closure",
-    downloaded_file_path = "polymer_closure.js",
-    sha256 = "4d63a36dcca040475bd6deb815b9a600bd686e1413ac1ebd4b04516edd675020",
-    urls = ["https://raw.githubusercontent.com/google/closure-compiler/35d2b3340ff23a69441f10fa3bc820691c2942f2/contrib/externs/polymer-1.0.js"],
+load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
+
+build_bazel_rules_nodejs_dependencies()
+
+# This is required just because we have a dependency on @bazel/concatjs.
+# We don't actually use any of this web_testing stuff.
+# TODO: Remove this dependency.
+http_archive(
+    name = "io_bazel_rules_webtesting",
+    sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
+    urls = [
+        "https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz",
+    ],
 )
 
-load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
 
-# Prevent redundant loading of dependencies.
-# TODO(davido): Omit re-fetching ancient args4j version when these PRs are merged:
-# https://github.com/bazelbuild/rules_closure/pull/262
-# https://github.com/google/closure-templates/pull/155
-rules_closure_dependencies(
-    omit_aopalliance = True,
-    omit_javax_inject = True,
-    omit_rules_cc = True,
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+web_test_repositories()
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl", "browser_repositories")
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+browser_repositories(
+    chromium = True,
+    firefox = True,
 )
 
-rules_closure_toolchains()
-
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "b34cbe1a7514f5f5487c3bfee7340a4496713ddf4f119f7a225583d6cafd793a",
+    sha256 = "a8d6b1b354d371a646d2f7927319974e0f9e52f73a2452d2b3877118169eb6bb",
     urls = [
-        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
     ],
 )
 
@@ -108,8 +118,11 @@
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
-    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
+    sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+    ],
 )
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
@@ -156,36 +169,6 @@
     sha1 = "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
 )
 
-GUICE_VERS = "4.2.3"
-
-GUICE_LIBRARY_SHA256 = "5168f5e7383f978c1b4154ac777b78edd8ac214bb9f9afdb92921c8d156483d3"
-
-http_file(
-    name = "guice-library-no-aop",
-    canonical_id = "guice-library-no-aop-" + GUICE_VERS + ".jar-" + GUICE_LIBRARY_SHA256,
-    downloaded_file_path = "guice-library-no-aop.jar",
-    sha256 = GUICE_LIBRARY_SHA256,
-    urls = [
-        "https://repo1.maven.org/maven2/com/google/inject/guice/" +
-        GUICE_VERS +
-        "/guice-" +
-        GUICE_VERS +
-        "-no_aop.jar",
-    ],
-)
-
-maven_jar(
-    name = "guice-assistedinject",
-    artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
-    sha1 = "acbfddc556ee9496293ed1df250cc378f331d854",
-)
-
-maven_jar(
-    name = "guice-servlet",
-    artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS,
-    sha1 = "8d6e7e35eac4fb5e7df19c55b3bc23fa51b10a11",
-)
-
 maven_jar(
     name = "javax_inject",
     artifact = "javax.inject:javax.inject:1",
@@ -194,8 +177,8 @@
 
 maven_jar(
     name = "servlet-api",
-    artifact = "org.apache.tomcat:tomcat-servlet-api:8.5.23",
-    sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
+    artifact = "javax.servlet:javax.servlet-api:3.1.0",
+    sha1 = "3cd63d075497751784b2fa84be59432f4905bf7c",
 )
 
 # JGit's transitive dependencies
@@ -284,38 +267,6 @@
     sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
 )
 
-SLF4J_VERS = "1.7.26"
-
-maven_jar(
-    name = "log-api",
-    artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
-    sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
-)
-
-maven_jar(
-    name = "log-ext",
-    artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
-    sha1 = "31cdf122e000322e9efcb38913e9ab07825b17ef",
-)
-
-maven_jar(
-    name = "impl-log4j",
-    artifact = "org.slf4j:slf4j-log4j12:" + SLF4J_VERS,
-    sha1 = "12f5c685b71c3027fd28bcf90528ec4ec74bf818",
-)
-
-maven_jar(
-    name = "jcl-over-slf4j",
-    artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
-    sha1 = "33fbc2d93de829fa5e263c5ce97f5eab8f57d53e",
-)
-
-maven_jar(
-    name = "log4j",
-    artifact = "log4j:log4j:1.2.17",
-    sha1 = "5af35056b4d257e4b64b9e8069c0746e8b08629f",
-)
-
 maven_jar(
     name = "json-smart",
     artifact = "net.minidev:json-smart:1.1.1",
@@ -323,7 +274,7 @@
 )
 
 maven_jar(
-    name = "args4j-intern",
+    name = "args4j",
     artifact = "args4j:args4j:2.33",
     sha1 = "bd87a75374a6d6523de82fef51fc3cfe9baf9fc9",
 )
@@ -950,12 +901,6 @@
 )
 
 maven_jar(
-    name = "commons-io",
-    artifact = "commons-io:commons-io:2.2",
-    sha1 = "83b5b8a7ba1c08f9e8c8ff2373724e33d3c1e22a",
-)
-
-maven_jar(
     name = "asciidoctor",
     artifact = "org.asciidoctor:asciidoctorj:1.5.7",
     sha1 = "8e8c1d8fc6144405700dd8df3b177f2801ac5987",
@@ -999,6 +944,60 @@
     sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
 )
 
+load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+
+node_repositories(
+    node_version = "16.13.2",
+    yarn_version = "1.22.17",
+)
+
+yarn_install(
+    name = "npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
+    package_json = "//:package.json",
+    symlink_node_modules = True,
+    yarn_lock = "//:yarn.lock",
+)
+
+yarn_install(
+    name = "ui_npm",
+    args = ["--prod"],
+    exports_directories_only = False,
+    frozen_lockfile = False,
+    package_json = "//:polygerrit-ui/app/package.json",
+    symlink_node_modules = True,
+    yarn_lock = "//:polygerrit-ui/app/yarn.lock",
+)
+
+yarn_install(
+    name = "ui_dev_npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
+    package_json = "//:polygerrit-ui/package.json",
+    symlink_node_modules = True,
+    yarn_lock = "//:polygerrit-ui/yarn.lock",
+)
+
+yarn_install(
+    name = "tools_npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
+    package_json = "//:tools/node_tools/package.json",
+    symlink_node_modules = True,
+    yarn_lock = "//:tools/node_tools/yarn.lock",
+)
+
+yarn_install(
+    name = "plugins_npm",
+    args = ["--prod"],
+    exports_directories_only = False,
+    frozen_lockfile = False,
+    package_json = "//:plugins/package.json",
+    symlink_node_modules = True,
+    yarn_lock = "//:plugins/yarn.lock",
+)
+
 load("//tools/bzl:js.bzl", "bower_archive", "npm_binary")
 
 # NPM binaries bundled along with their dependencies.
@@ -1163,8 +1162,8 @@
 bower_archive(
     name = "codemirror-minified",
     package = "Dominator008/codemirror-minified",
-    sha1 = "d00f3b97345772d5a7790f206cb1e3c22e96caf6",
-    version = "5.50.2",
+    sha1 = "904bae2a8716087fd21e92324e8a136a0c4a99b7",
+    version = "5.62.2",
 )
 
 # bower test stuff
@@ -1190,40 +1189,4 @@
     version = "6.5.1",
 )
 
-load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
-
-yarn_install(
-    name = "npm",
-    package_json = "//:package.json",
-    yarn_lock = "//:yarn.lock",
-)
-
-yarn_install(
-    name = "ui_npm",
-    args = ["--prod"],
-    package_json = "//:polygerrit-ui/app/package.json",
-    yarn_lock = "//:polygerrit-ui/app/yarn.lock",
-)
-
-yarn_install(
-    name = "ui_dev_npm",
-    package_json = "//:polygerrit-ui/package.json",
-    yarn_lock = "//:polygerrit-ui/yarn.lock",
-)
-
-yarn_install(
-    name = "tools_npm",
-    package_json = "//:tools/node_tools/package.json",
-    yarn_lock = "//:tools/node_tools/yarn.lock",
-)
-
-# Install all Bazel dependencies needed for npm packages that supply Bazel rules
-load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
-
-install_bazel_dependencies()
-
-load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
-
-ts_setup_workspace()
-
 external_plugin_deps()
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 549946a..23641e3 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -22,6 +22,7 @@
     srcs = [":query"],
     visibility = [
         "//java/com/google/gerrit/index:__subpackages__",
+        "//java/com/google/gerrit/server:__subpackages__",
         "//javatests/com/google/gerrit:__subpackages__",
         "//javatests/com/google/gerrit/index:__pkg__",
         "//plugins:__pkg__",
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index bc6a71c..482c804 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -49,21 +49,23 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.LabelFunction;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.PermissionRule;
+import com.google.gerrit.entities.PermissionRule.Action;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -89,8 +91,6 @@
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -436,18 +436,20 @@
 
     baseConfig.setInt("index", null, "batchThreads", -1);
 
-    baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
     Module module = createModule();
+    Module auditModule = createAuditModule();
     Module sshModule = createSshModule();
     if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
       if (commonServer == null) {
         commonServer =
-            GerritServer.initAndStart(temporaryFolder, classDesc, baseConfig, module, sshModule);
+            GerritServer.initAndStart(
+                temporaryFolder, classDesc, baseConfig, module, auditModule, sshModule);
       }
       server = commonServer;
     } else {
       server =
-          GerritServer.initAndStart(temporaryFolder, methodDesc, baseConfig, module, sshModule);
+          GerritServer.initAndStart(
+              temporaryFolder, methodDesc, baseConfig, module, auditModule, sshModule);
     }
 
     server.getTestInjector().injectMembers(this);
@@ -540,6 +542,11 @@
     return null;
   }
 
+  /** Override to bind an alternative audit Guice module */
+  public Module createAuditModule() {
+    return null;
+  }
+
   /** Override to bind an additional Guice module for SSH injector */
   public Module createSshModule() {
     return null;
@@ -819,6 +826,24 @@
   private static final List<Character> RANDOM =
       Chars.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
 
+  protected PushOneCommit.Result amendChangeWithUploader(
+      PushOneCommit.Result change, Project.NameKey projectName, TestAccount account)
+      throws Exception {
+    TestRepository<InMemoryRepository> repo = cloneProject(projectName, account);
+    GitUtil.fetch(repo, "refs/*:refs/*");
+    repo.reset(change.getCommit());
+    PushOneCommit.Result result =
+        amendChange(
+            change.getChangeId(),
+            "refs/for/master",
+            account,
+            repo,
+            "new subject",
+            "new file",
+            "new content");
+    return result;
+  }
+
   protected PushOneCommit.Result amendChange(String changeId) throws Exception {
     return amendChange(changeId, "refs/for/master", admin, testRepo);
   }
@@ -1000,7 +1025,7 @@
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = projectConfigFactory.read(md);
-      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, value);
+      config.updateProject(p -> p.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, value));
       config.commit(md);
       projectCache.evictAndReindex(config.getProject());
     }
@@ -1009,7 +1034,7 @@
   protected void setRequireChangeId(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = projectConfigFactory.read(md);
-      config.getProject().setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value);
+      config.updateProject(p -> p.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value));
       config.commit(md);
       projectCache.evictAndReindex(config.getProject());
     }
@@ -1230,11 +1255,15 @@
       String ref,
       boolean exclusive,
       String... permissionNames) {
-    ProjectConfig cfg = projectCache.get(project).orElseThrow(illegalState(project)).getConfig();
-    AccessSection accessSection = cfg.getAccessSection(ref);
-    assertThat(accessSection).isNotNull();
+    Optional<AccessSection> accessSection =
+        projectCache
+            .get(project)
+            .orElseThrow(illegalState(project))
+            .getConfig()
+            .getAccessSection(ref);
+    assertThat(accessSection).isPresent();
     for (String permissionName : permissionNames) {
-      Permission permission = accessSection.getPermission(permissionName);
+      Permission permission = accessSection.get().getPermission(permissionName);
       assertPermission(permission, permissionName, exclusive, null);
       assertPermissionRule(
           permission.getRule(groupReference), groupReference, Action.ALLOW, false, 0, 0);
@@ -1274,7 +1303,7 @@
 
   protected GroupReference groupRef(AccountGroup.UUID groupUuid) {
     GroupDescription.Basic groupDescription = groupBackend.get(groupUuid);
-    return new GroupReference(groupDescription.getGroupUUID(), groupDescription.getName());
+    return GroupReference.create(groupDescription.getGroupUUID(), groupDescription.getName());
   }
 
   protected InternalGroup group(String groupName) {
@@ -1286,7 +1315,7 @@
   protected GroupReference groupRef(String groupName) {
     InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
     assertThat(group).isNotNull();
-    return new GroupReference(group.getGroupUUID(), group.getName());
+    return GroupReference.create(group.getGroupUUID(), group.getName());
   }
 
   protected AccountGroup.UUID groupUuid(String groupName) {
@@ -1382,6 +1411,7 @@
           pwi.filter = filter;
           pwi.notifyAbandonedChanges = true;
           pwi.notifyNewChanges = true;
+          pwi.notifyNewPatchSets = true;
           pwi.notifyAllComments = true;
         });
   }
@@ -1459,10 +1489,10 @@
       LabelValue... value)
       throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType labelType = label(label, value);
+      LabelType.Builder labelType = label(label, value).toBuilder();
       labelType.setFunction(func);
-      labelType.setRefPatterns(refPatterns);
-      u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+      labelType.setRefPatterns(ImmutableList.copyOf(refPatterns));
+      u.getConfig().upsertLabelType(labelType.build());
       u.save();
     }
   }
@@ -1470,10 +1500,11 @@
   protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
+          .updateProject(
+              p ->
+                  p.setBooleanConfig(
+                      BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+                      InheritableBoolean.TRUE));
       u.save();
     }
   }
@@ -1525,7 +1556,8 @@
 
   protected List<CommentInfo> getChangeSortedComments(int changeNum) throws Exception {
     List<CommentInfo> comments = new ArrayList<>();
-    Map<String, List<CommentInfo>> commentsMap = gApi.changes().id(changeNum).comments();
+    Map<String, List<CommentInfo>> commentsMap =
+        gApi.changes().id(changeNum).commentsRequest().get();
     for (Map.Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) {
       for (CommentInfo c : e.getValue()) {
         c.path = e.getKey(); // Set the comment's path field.
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index bb3901e..452df67 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -28,6 +28,10 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.EmailHeader.AddressList;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
@@ -36,10 +40,6 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
-import com.google.gerrit.mail.EmailHeader.AddressList;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index 020602b..a91bc49 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -21,26 +21,36 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.query.change.ChangeData;
 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.Inject;
 import com.google.inject.Module;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import org.kohsuke.args4j.Option;
 
 public class AbstractPluginFieldsTest extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+
   protected static class MyInfo extends PluginDefinedInfo {
     @Nullable String theAttribute;
 
@@ -91,6 +101,70 @@
     }
   }
 
+  protected static class PluginDefinedSimpleAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .toInstance(
+              (cds, bp, p) -> {
+                Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
+                cds.forEach(cd -> out.put(cd.getId(), new MyInfo("change " + cd.getId())));
+                return out;
+              });
+    }
+  }
+
+  protected static class PluginDefinedBulkExceptionModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .toInstance(
+              (cds, bp, p) -> {
+                throw new RuntimeException("Sample Exception");
+              });
+    }
+  }
+
+  protected static class PluginDefinedChangesByCommitBulkAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .toInstance(
+              (cds, bp, p) -> {
+                Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
+                cds.forEach(
+                    cd ->
+                        out.put(
+                            cd.getId(),
+                            !cd.commitMessage().contains("no-info")
+                                ? new MyInfo("change " + cd.getId())
+                                : null));
+                return out;
+              });
+    }
+  }
+
+  protected static class PluginDefinedSingleCallBulkAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .to(SingleCallBulkFactoryAttribute.class);
+    }
+  }
+
+  protected static class SingleCallBulkFactoryAttribute implements ChangePluginDefinedInfoFactory {
+    public static int timesCreateCalled = 0;
+
+    @Override
+    public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+        Collection<ChangeData> cds, DynamicOptions.BeanProvider beanProvider, String plugin) {
+      timesCreateCalled++;
+      Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
+      cds.forEach(cd -> out.put(cd.getId(), new MyInfo("change " + cd.getId())));
+      return out;
+    }
+  }
+
   private static class MyOptions implements DynamicBean {
     @Option(name = "--opt")
     private String opt;
@@ -111,6 +185,32 @@
     }
   }
 
+  public static class BulkAttributeFactoryWithOption implements ChangePluginDefinedInfoFactory {
+    protected MyOptions opts;
+
+    @Override
+    public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+        Collection<ChangeData> cds, DynamicOptions.BeanProvider beanProvider, String plugin) {
+      if (opts == null) {
+        opts = (MyOptions) beanProvider.getDynamicBean(plugin);
+      }
+      Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
+      cds.forEach(cd -> out.put(cd.getId(), new MyInfo("opt " + opts.opt)));
+      return out;
+    }
+  }
+
+  protected static class PluginDefinedOptionAttributeModule extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .to(BulkAttributeFactoryWithOption.class);
+      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();
@@ -138,6 +238,113 @@
     assertThat(getter.call(id)).isNull();
   }
 
+  protected void getSingleChangeWithPluginDefinedBulkAttribute(BulkPluginInfoGetterWithId getter)
+      throws Exception {
+    Change.Id id = createChange().getChange().getId();
+
+    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call(id);
+    assertThat(pluginInfos.get(id)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
+      pluginInfos = getter.call(id);
+      assertThat(pluginInfos.get(id)).containsExactly(new MyInfo("my-plugin", "change " + id));
+    }
+
+    pluginInfos = getter.call(id);
+    assertThat(pluginInfos.get(id)).isNull();
+  }
+
+  protected void getMultipleChangesWithPluginDefinedBulkAttribute(BulkPluginInfoGetter getter)
+      throws Exception {
+    Change.Id id1 = createChange().getChange().getId();
+    Change.Id id2 = createChange().getChange().getId();
+
+    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
+      pluginInfos = getter.call();
+      assertThat(pluginInfos.get(id1)).containsExactly(new MyInfo("my-plugin", "change " + id1));
+      assertThat(pluginInfos.get(id2)).containsExactly(new MyInfo("my-plugin", "change " + id2));
+    }
+
+    pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+  }
+
+  protected void getChangesByCommitMessageWithPluginDefinedBulkAttribute(
+      BulkPluginInfoGetter getter) throws Exception {
+    Change.Id changeWithNoInfo = changeOperations.newChange().commitMessage("no-info").create();
+    Change.Id changeWithInfo = changeOperations.newChange().commitMessage("info").create();
+
+    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
+    assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
+    assertThat(pluginInfos.get(changeWithInfo)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedChangesByCommitBulkAttributeModule.class)) {
+      pluginInfos = getter.call();
+      assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
+      assertThat(pluginInfos.get(changeWithInfo))
+          .containsExactly(new MyInfo("my-plugin", "change " + changeWithInfo));
+    }
+
+    pluginInfos = getter.call();
+    assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
+    assertThat(pluginInfos.get(changeWithInfo)).isNull();
+  }
+
+  protected void getMultipleChangesWithPluginDefinedBulkAndChangeAttributes(
+      BulkPluginInfoGetter getter) throws Exception {
+    Change.Id id1 = createChange().getChange().getId();
+    Change.Id id2 = createChange().getChange().getId();
+
+    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+
+    try (AutoCloseable ignored =
+            installPlugin("my-plugin-1", PluginDefinedSimpleAttributeModule.class);
+        AutoCloseable ignored1 = installPlugin("my-plugin-2", SimpleAttributeModule.class)) {
+      pluginInfos = getter.call();
+      assertThat(pluginInfos.get(id1)).contains(new MyInfo("my-plugin-1", "change " + id1));
+      assertThat(pluginInfos.get(id1)).contains(new MyInfo("my-plugin-2", "change " + id1));
+      assertThat(pluginInfos.get(id2)).contains(new MyInfo("my-plugin-1", "change " + id2));
+      assertThat(pluginInfos.get(id2)).contains(new MyInfo("my-plugin-2", "change " + id2));
+    }
+
+    pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+  }
+
+  protected void getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
+      BulkPluginInfoGetter getter) throws Exception {
+    Change.Id id1 = createChange().getChange().getId();
+    Change.Id id2 = createChange().getChange().getId();
+    int timesCalled = SingleCallBulkFactoryAttribute.timesCreateCalled;
+
+    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedSingleCallBulkAttributeModule.class)) {
+      pluginInfos = getter.call();
+      assertThat(pluginInfos.get(id1)).containsExactly(new MyInfo("my-plugin", "change " + id1));
+      assertThat(pluginInfos.get(id2)).containsExactly(new MyInfo("my-plugin", "change " + id2));
+      assertThat(SingleCallBulkFactoryAttribute.timesCreateCalled).isEqualTo(timesCalled + 1);
+    }
+
+    pluginInfos = getter.call();
+    assertThat(pluginInfos.get(id1)).isNull();
+    assertThat(pluginInfos.get(id2)).isNull();
+  }
+
   protected void getChangeWithOption(
       PluginInfoGetter getterWithoutOptions, PluginInfoGetterWithOptions getterWithOptions)
       throws Exception {
@@ -154,17 +361,61 @@
     assertThat(getterWithoutOptions.call(id)).isNull();
   }
 
-  protected static List<MyInfo> pluginInfoFromSingletonList(List<ChangeInfo> changeInfos) {
+  protected void getChangeWithPluginDefinedBulkAttributeOption(
+      BulkPluginInfoGetterWithId getterWithoutOptions,
+      BulkPluginInfoGetterWithIdAndOptions getterWithOptions)
+      throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getterWithoutOptions.call(id).get(id)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedOptionAttributeModule.class)) {
+      assertThat(getterWithoutOptions.call(id).get(id))
+          .containsExactly(new MyInfo("my-plugin", "opt null"));
+      assertThat(
+              getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")).get(id))
+          .containsExactly(new MyInfo("my-plugin", "opt foo"));
+    }
+
+    assertThat(getterWithoutOptions.call(id).get(id)).isNull();
+  }
+
+  protected void getChangeWithPluginDefinedBulkAttributeWithException(
+      BulkPluginInfoGetterWithId getter) throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getter.call(id).get(id)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedBulkExceptionModule.class)) {
+      PluginDefinedInfo errorInfo = new PluginDefinedInfo();
+      List<PluginDefinedInfo> outputInfos = getter.call(id).get(id);
+      assertThat(outputInfos).hasSize(1);
+      assertThat(outputInfos.get(0).name).isEqualTo("my-plugin");
+      assertThat(outputInfos.get(0).message).isEqualTo("Something went wrong in plugin: my-plugin");
+    }
+
+    assertThat(getter.call(id).get(id)).isNull();
+  }
+
+  protected static List<PluginDefinedInfo> pluginInfoFromSingletonList(
+      List<ChangeInfo> changeInfos) {
     assertThat(changeInfos).hasSize(1);
     return pluginInfoFromChangeInfo(changeInfos.get(0));
   }
 
-  protected static List<MyInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
+  protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
     List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
     if (pluginInfo == null) {
       return null;
     }
-    return pluginInfo.stream().map(MyInfo.class::cast).collect(toImmutableList());
+    return pluginInfo.stream().map(PluginDefinedInfo.class::cast).collect(toImmutableList());
+  }
+
+  protected static Map<Change.Id, List<PluginDefinedInfo>> pluginInfosFromChangeInfos(
+      List<ChangeInfo> changeInfos) {
+    Map<Change.Id, List<PluginDefinedInfo>> out = new HashMap<>();
+    changeInfos.forEach(ci -> out.put(Change.id(ci._number), pluginInfoFromChangeInfo(ci)));
+    return out;
   }
 
   /**
@@ -180,7 +431,8 @@
    * @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) {
+  protected static List<PluginDefinedInfo> decodeRawPluginsList(
+      Gson gson, @Nullable Object plugins) {
     if (plugins == null) {
       return null;
     }
@@ -188,14 +440,44 @@
     return gson.fromJson(gson.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
   }
 
+  protected static Map<Change.Id, List<PluginDefinedInfo>> getPluginInfosFromChangeInfos(
+      Gson gson, List<Map<String, Object>> changeInfos) {
+    Map<Change.Id, List<PluginDefinedInfo>> out = new HashMap<>();
+    changeInfos.forEach(
+        change -> {
+          Double changeId =
+              (Double)
+                  (change.get("number") != null ? change.get("number") : change.get("_number"));
+          out.put(
+              Change.id(changeId.intValue()), decodeRawPluginsList(gson, change.get("plugins")));
+        });
+    return out;
+  }
+
   @FunctionalInterface
   protected interface PluginInfoGetter {
-    List<MyInfo> call(Change.Id id) throws Exception;
+    List<PluginDefinedInfo> call(Change.Id id) throws Exception;
+  }
+
+  @FunctionalInterface
+  protected interface BulkPluginInfoGetter {
+    Map<Change.Id, List<PluginDefinedInfo>> call() throws Exception;
+  }
+
+  @FunctionalInterface
+  protected interface BulkPluginInfoGetterWithId {
+    Map<Change.Id, List<PluginDefinedInfo>> call(Change.Id id) throws Exception;
+  }
+
+  @FunctionalInterface
+  protected interface BulkPluginInfoGetterWithIdAndOptions {
+    Map<Change.Id, List<PluginDefinedInfo>> call(
+        Change.Id id, ImmutableListMultimap<String, String> pluginOptions) throws Exception;
   }
 
   @FunctionalInterface
   protected interface PluginInfoGetterWithOptions {
-    List<MyInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
+    List<PluginDefinedInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
         throws Exception;
   }
 }
diff --git a/java/com/google/gerrit/acceptance/AbstractPredicateTest.java b/java/com/google/gerrit/acceptance/AbstractPredicateTest.java
index c9fd3fb..c629959 100644
--- a/java/com/google/gerrit/acceptance/AbstractPredicateTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPredicateTest.java
@@ -41,10 +41,6 @@
   public static final String PLUGIN_NAME = "my-plugin";
   public static final Gson GSON = OutputFormat.JSON.newGson();
 
-  public static class MyInfo extends PluginDefinedInfo {
-    public String message;
-  }
-
   protected static class PluginModule extends AbstractModule {
     @Override
     public void configure() {
@@ -77,7 +73,7 @@
     public PluginDefinedInfo create(
         ChangeData cd, DynamicOptions.BeanProvider beanProvider, String plugin) {
       MyQueryOptions options = (MyQueryOptions) beanProvider.getDynamicBean(plugin);
-      MyInfo myInfo = new MyInfo();
+      PluginDefinedInfo myInfo = new PluginDefinedInfo();
       if (options.sample) {
         try {
           Predicate<ChangeData> predicate = queryBuilderProvider.get().parse("label:Code-Review+2");
@@ -94,11 +90,12 @@
     }
   }
 
-  protected static List<MyInfo> decodeRawPluginsList(@Nullable Object plugins) {
+  protected static List<PluginDefinedInfo> decodeRawPluginsList(@Nullable Object plugins) {
     if (plugins == null) {
       return Collections.emptyList();
     }
     checkArgument(plugins instanceof List, "not a list: %s", plugins);
-    return GSON.fromJson(GSON.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+    return GSON.fromJson(
+        GSON.toJson(plugins), new TypeToken<List<PluginDefinedInfo>>() {}.getType());
   }
 }
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index 44e2d2d..3ab1cec 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -101,6 +101,7 @@
                     .setPreferredEmail(email)
                     .addExternalIds(extIds));
 
+    ImmutableList.Builder<String> tags = ImmutableList.builder();
     if (groupNames != null) {
       for (String n : groupNames) {
         AccountGroup.NameKey k = AccountGroup.nameKey(n);
@@ -109,10 +110,14 @@
           throw new NoSuchGroupException(n);
         }
         addGroupMember(group.get().getGroupUUID(), id);
+        if ("Service Users".equals(n)) {
+          tags.add("SERVICE_USER");
+        }
       }
     }
 
-    account = TestAccount.create(id, username, email, fullName, displayName, httpPass);
+    account =
+        TestAccount.create(id, username, email, fullName, displayName, httpPass, tags.build());
     if (username != null) {
       accounts.put(username, account);
     }
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 9d8bc57..db0dc84 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -64,6 +64,7 @@
     "//java/com/google/gerrit/acceptance/config",
     "//java/com/google/gerrit/acceptance/testsuite/project",
     "//java/com/google/gerrit/server/fixes/testing",
+    "//java/com/google/gerrit/server/data",
     "//java/com/google/gerrit/server/group/testing",
     "//java/com/google/gerrit/server/project/testing:project-test-util",
     "//java/com/google/gerrit/testing:gerrit-test-util",
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index cfe7964..a5d8d19 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.change.ChangeETagComputation;
+import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
@@ -79,6 +80,7 @@
   private final DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners;
   private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
   private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
+  private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
 
   @Inject
   ExtensionRegistry(
@@ -107,7 +109,8 @@
       DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners,
       DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners,
       DynamicMap<CapabilityDefinition> capabilityDefinitions,
-      DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions) {
+      DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions,
+      DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
     this.accountIndexedListeners = accountIndexedListeners;
     this.changeIndexedListeners = changeIndexedListeners;
     this.groupIndexedListeners = groupIndexedListeners;
@@ -134,6 +137,7 @@
     this.workInProgressStateChangedListeners = workInProgressStateChangedListeners;
     this.capabilityDefinitions = capabilityDefinitions;
     this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
+    this.pluginConfigEntries = pluginConfigEntries;
   }
 
   public Registration newRegistration() {
@@ -254,6 +258,10 @@
       return add(pluginProjectPermissionDefinitions, pluginProjectPermissionDefinition, exportName);
     }
 
+    public Registration add(ProjectConfigEntry pluginConfigEntry, String exportName) {
+      return add(pluginConfigEntries, pluginConfigEntry, exportName);
+    }
+
     private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
       return add(dynamicSet, extension, "gerrit");
     }
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index f1598e7..cfc0ce4 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -30,6 +30,12 @@
 import com.google.gerrit.acceptance.config.GlobalPluginConfigs;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.PerCommentOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.PerDraftCommentOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.PerPatchsetOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.PerRobotCommentOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -327,6 +333,7 @@
       Description desc,
       Config baseConfig,
       @Nullable Module testSysModule,
+      @Nullable Module testAuditModule,
       @Nullable Module testSshModule)
       throws Exception {
     Path site = temporaryFolder.newFolder().toPath();
@@ -334,7 +341,7 @@
       if (!desc.memory()) {
         init(desc, baseConfig, site);
       }
-      return start(desc, baseConfig, site, testSysModule, testSshModule, null);
+      return start(desc, baseConfig, site, testSysModule, testAuditModule, testSshModule, null);
     } catch (Exception e) {
       throw e;
     }
@@ -363,6 +370,7 @@
       Config baseConfig,
       Path site,
       @Nullable Module testSysModule,
+      @Nullable Module testAuditModule,
       @Nullable Module testSshModule,
       @Nullable InMemoryRepositoryManager inMemoryRepoManager,
       String... additionalArgs)
@@ -382,7 +390,8 @@
             },
             site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
-    daemon.setAuditEventModuleForTesting(new FakeGroupAuditService.Module());
+    daemon.setAuditEventModuleForTesting(
+        MoreObjects.firstNonNull(testAuditModule, new FakeGroupAuditService.Module()));
     if (testSysModule != null) {
       daemon.addAdditionalSysModuleForTesting(testSysModule);
     }
@@ -492,7 +501,6 @@
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setBoolean("sendemail", null, "enable", true);
     cfg.setInt("sendemail", null, "threadPoolSize", 0);
-    cfg.setInt("cache", "projects", "checkFrequency", 0);
     cfg.setInt("plugins", null, "checkFrequency", 0);
 
     cfg.setInt("sshd", null, "threads", 1);
@@ -516,6 +524,11 @@
             bind(GroupOperations.class).to(GroupOperationsImpl.class);
             bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
             bind(RequestScopeOperations.class).to(RequestScopeOperationsImpl.class);
+            bind(ChangeOperations.class).to(ChangeOperationsImpl.class);
+            factory(PerPatchsetOperationsImpl.Factory.class);
+            factory(PerCommentOperationsImpl.Factory.class);
+            factory(PerDraftCommentOperationsImpl.Factory.class);
+            factory(PerRobotCommentOperationsImpl.Factory.class);
             factory(PushOneCommit.Factory.class);
             install(InProcessProtocol.module());
             install(new NoSshModule());
@@ -614,7 +627,7 @@
 
     server.close();
     server.daemon.stop();
-    return start(server.desc, cfg, site, null, null, inMemoryRepoManager);
+    return start(server.desc, cfg, site, null, null, null, inMemoryRepoManager);
   }
 
   public static GerritServer restart(
@@ -631,7 +644,7 @@
 
     server.close();
     server.daemon.stop();
-    return start(server.desc, cfg, site, testSysModule, testSshModule, inMemoryRepoManager);
+    return start(server.desc, cfg, site, testSysModule, null, testSshModule, inMemoryRepoManager);
   }
 
   private static boolean hasBinding(Injector injector, Class<?> clazz) {
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 3ccbe4d..afd451a 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -25,6 +25,8 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -91,6 +93,14 @@
         @Assisted String subject,
         @Assisted Map<String, String> files);
 
+    @UsedAt(Project.PLUGIN_CODE_OWNERS)
+    PushOneCommit create(
+        PersonIdent i,
+        TestRepository<?> testRepo,
+        @Assisted("subject") String subject,
+        @Assisted Map<String, String> files,
+        @Assisted("changeId") String changeId);
+
     PushOneCommit create(
         PersonIdent i,
         TestRepository<?> testRepo,
@@ -227,15 +237,16 @@
         changeId);
   }
 
-  private PushOneCommit(
+  @AssistedInject
+  PushOneCommit(
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      PersonIdent i,
-      TestRepository<?> testRepo,
-      String subject,
-      Map<String, String> files,
-      String changeId)
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
+      @Assisted("subject") String subject,
+      @Assisted Map<String, String> files,
+      @Nullable @Assisted("changeId") String changeId)
       throws Exception {
     this.testRepo = testRepo;
     this.notesFactory = notesFactory;
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
index b985e40..0b2282e 100644
--- a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 6698657..21b216a 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -25,8 +25,12 @@
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.KeyPair;
 import com.jcraft.jsch.Session;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
 import java.util.Scanner;
 
 public class SshSession {
@@ -81,6 +85,20 @@
     }
   }
 
+  public Reader execAndReturnReader(String command) throws Exception {
+    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
+    channel.setCommand(command);
+    channel.connect();
+
+    return new InputStreamReader(channel.getInputStream(), StandardCharsets.UTF_8) {
+      @Override
+      public void close() throws IOException {
+        super.close();
+        channel.disconnect();
+      }
+    };
+  }
+
   private boolean hasError() {
     return error != null;
   }
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 43fe4eb..dcb49a5 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -187,7 +187,14 @@
   private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
       throws Exception {
     return GerritServer.start(
-        serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, null, additionalArgs);
+        serverDesc,
+        baseConfig,
+        sitePaths.site_path,
+        testSysModule,
+        null,
+        null,
+        null,
+        additionalArgs);
   }
 
   protected static void runGerrit(String... args) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index a7a4a89..d5908f4 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -22,7 +22,7 @@
 import com.google.common.net.InetAddresses;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
 import org.apache.http.client.utils.URIBuilder;
@@ -48,8 +48,10 @@
       @Nullable String email,
       @Nullable String fullName,
       @Nullable String displayName,
-      @Nullable String httpPassword) {
-    return new AutoValue_TestAccount(id, username, email, fullName, displayName, httpPassword);
+      @Nullable String httpPassword,
+      ImmutableList<String> tags) {
+    return new AutoValue_TestAccount(
+        id, username, email, fullName, displayName, httpPassword, tags);
   }
 
   public abstract Account.Id id();
@@ -69,6 +71,8 @@
   @Nullable
   public abstract String httpPassword();
 
+  public abstract ImmutableList<String> tags();
+
   public PersonIdent newIdent() {
     return new PersonIdent(fullName(), email());
   }
diff --git a/java/com/google/gerrit/acceptance/WaitUtil.java b/java/com/google/gerrit/acceptance/WaitUtil.java
new file mode 100644
index 0000000..6040f16
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/WaitUtil.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.common.base.Stopwatch;
+import java.time.Duration;
+import java.util.function.Supplier;
+
+public class WaitUtil {
+  public static void waitUntil(Supplier<Boolean> waitCondition, Duration timeout)
+      throws InterruptedException {
+    Stopwatch stopwatch = Stopwatch.createStarted();
+    while (!waitCondition.get()) {
+      if (stopwatch.elapsed().compareTo(timeout) > 0) {
+        throw new InterruptedException();
+      }
+      MILLISECONDS.sleep(50);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/WaitUtilTest.java b/java/com/google/gerrit/acceptance/WaitUtilTest.java
new file mode 100644
index 0000000..565da9c
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/WaitUtilTest.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.acceptance;
+
+import static com.google.gerrit.acceptance.WaitUtil.waitUntil;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import java.time.Duration;
+import org.junit.Test;
+
+public class WaitUtilTest {
+
+  @Test
+  public void shouldFailWhenConditionNotMetWithinTimeout() throws Exception {
+    assertThrows(
+        InterruptedException.class,
+        () -> waitUntil(() -> returnTrue() == false, Duration.ofSeconds(1)));
+  }
+
+  @Test
+  public void shouldNotFailWhenConditionIsMetWithinTimeout() throws Exception {
+    waitUntil(() -> returnTrue() == true, Duration.ofSeconds(1));
+  }
+
+  private static boolean returnTrue() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index f64d7a2..8c1eebd 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.acceptance.testsuite.account;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountState;
@@ -60,8 +63,8 @@
 
   private Account.Id createAccount(TestAccountCreation accountCreation) throws Exception {
     AccountsUpdate.AccountUpdater accountUpdater =
-        (account, updateBuilder) ->
-            fillBuilder(updateBuilder, accountCreation, account.account().id());
+        (accountState, updateBuilder) ->
+            fillBuilder(updateBuilder, accountCreation, accountState.account().id());
     AccountState createdAccount = createAccount(accountUpdater);
     return createdAccount.account().id();
   }
@@ -82,6 +85,11 @@
     accountCreation.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
     accountCreation.status().ifPresent(builder::setStatus);
     accountCreation.active().ifPresent(builder::setActive);
+    accountCreation
+        .secondaryEmails()
+        .forEach(
+            secondaryEmail ->
+                builder.addExternalId(ExternalId.createEmail(accountId, secondaryEmail)));
   }
 
   private static InternalAccountUpdate.Builder setPreferredEmail(
@@ -136,6 +144,7 @@
           .fullname(Optional.ofNullable(account.fullName()))
           .username(accountState.userName())
           .active(accountState.account().isActive())
+          .emails(ExternalId.getEmails(accountState.externalIds()).collect(toImmutableSet()))
           .build();
     }
 
@@ -147,7 +156,7 @@
     private void updateAccount(TestAccountUpdate accountUpdate)
         throws IOException, ConfigInvalidException {
       AccountsUpdate.AccountUpdater accountUpdater =
-          (account, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountId);
+          (accountState, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountState);
       Optional<AccountState> updatedAccount = updateAccount(accountUpdater);
       checkState(updatedAccount.isPresent(), "Tried to update non-existing test account");
     }
@@ -160,13 +169,58 @@
     private void fillBuilder(
         InternalAccountUpdate.Builder builder,
         TestAccountUpdate accountUpdate,
-        Account.Id accountId) {
+        AccountState accountState) {
       accountUpdate.fullname().ifPresent(builder::setFullName);
       accountUpdate.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
       String httpPassword = accountUpdate.httpPassword().orElse(null);
       accountUpdate.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
       accountUpdate.status().ifPresent(builder::setStatus);
       accountUpdate.active().ifPresent(builder::setActive);
+
+      ImmutableSet<String> secondaryEmails = getSecondaryEmails(accountUpdate, accountState);
+      ImmutableSet<String> newSecondaryEmails =
+          ImmutableSet.copyOf(accountUpdate.secondaryEmailsModification().apply(secondaryEmails));
+      if (!secondaryEmails.equals(newSecondaryEmails)) {
+        setSecondaryEmails(builder, accountUpdate, accountState, newSecondaryEmails);
+      }
+    }
+
+    private ImmutableSet<String> getSecondaryEmails(
+        TestAccountUpdate accountUpdate, AccountState accountState) {
+      ImmutableSet<String> allEmails =
+          ExternalId.getEmails(accountState.externalIds()).collect(toImmutableSet());
+      if (accountUpdate.preferredEmail().isPresent()) {
+        return ImmutableSet.copyOf(
+            Sets.difference(allEmails, ImmutableSet.of(accountUpdate.preferredEmail().get())));
+      } else if (accountState.account().preferredEmail() != null) {
+        return ImmutableSet.copyOf(
+            Sets.difference(allEmails, ImmutableSet.of(accountState.account().preferredEmail())));
+      }
+      return allEmails;
+    }
+
+    private void setSecondaryEmails(
+        InternalAccountUpdate.Builder builder,
+        TestAccountUpdate accountUpdate,
+        AccountState accountState,
+        ImmutableSet<String> newSecondaryEmails) {
+      // delete all external IDs of SCHEME_MAILTO scheme, then add back SCHEME_MAILTO external IDs
+      // for the new secondary emails and the preferred email
+      builder.deleteExternalIds(
+          accountState.externalIds().stream()
+              .filter(e -> e.isScheme(ExternalId.SCHEME_MAILTO))
+              .collect(toImmutableSet()));
+      builder.addExternalIds(
+          newSecondaryEmails.stream()
+              .map(secondaryEmail -> ExternalId.createEmail(accountId, secondaryEmail))
+              .collect(toImmutableSet()));
+      if (accountUpdate.preferredEmail().isPresent()) {
+        builder.addExternalId(
+            ExternalId.createEmail(accountId, accountUpdate.preferredEmail().get()));
+      } else if (accountState.account().preferredEmail() != null) {
+        builder.addExternalId(
+            ExternalId.createEmail(accountId, accountState.account().preferredEmail()));
+      }
     }
 
     @Override
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
index 2574d55..94b1cc4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.testsuite.account;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Account;
 import java.util.Optional;
 
@@ -30,6 +32,16 @@
 
   public abstract boolean active();
 
+  public abstract ImmutableSet<String> emails();
+
+  public ImmutableSet<String> secondaryEmails() {
+    if (!preferredEmail().isPresent()) {
+      return emails();
+    }
+
+    return ImmutableSet.copyOf(Sets.difference(emails(), ImmutableSet.of(preferredEmail().get())));
+  }
+
   static Builder builder() {
     return new AutoValue_TestAccount.Builder();
   }
@@ -46,6 +58,8 @@
 
     abstract Builder active(boolean active);
 
+    abstract Builder emails(ImmutableSet<String> emails);
+
     abstract TestAccount build();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index 983fec0..042dc9a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -14,10 +14,14 @@
 
 package com.google.gerrit.acceptance.testsuite.account;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.Account;
 import java.util.Optional;
+import java.util.Set;
 
 @AutoValue
 public abstract class TestAccountCreation {
@@ -33,6 +37,8 @@
 
   public abstract Optional<Boolean> active();
 
+  public abstract ImmutableSet<String> secondaryEmails();
+
   abstract ThrowingFunction<TestAccountCreation, Account.Id> accountCreator();
 
   public static Builder builder(ThrowingFunction<TestAccountCreation, Account.Id> accountCreator) {
@@ -83,14 +89,29 @@
       return active(false);
     }
 
+    public abstract Builder secondaryEmails(Set<String> secondaryEmails);
+
+    abstract ImmutableSet.Builder<String> secondaryEmailsBuilder();
+
+    public Builder addSecondaryEmail(String secondaryEmail) {
+      secondaryEmailsBuilder().add(secondaryEmail);
+      return this;
+    }
+
     abstract Builder accountCreator(
         ThrowingFunction<TestAccountCreation, Account.Id> accountCreator);
 
     abstract TestAccountCreation autoBuild();
 
     public Account.Id create() {
-      TestAccountCreation accountUpdate = autoBuild();
-      return accountUpdate.accountCreator().applyAndThrowSilently(accountUpdate);
+      TestAccountCreation accountCreation = autoBuild();
+      if (accountCreation.preferredEmail().isPresent()) {
+        checkState(
+            !accountCreation.secondaryEmails().contains(accountCreation.preferredEmail().get()),
+            "preferred email %s cannot be secondary email at the same time",
+            accountCreation.preferredEmail().get());
+      }
+      return accountCreation.accountCreator().applyAndThrowSilently(accountCreation);
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
index da599e7..46988eb 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
@@ -15,8 +15,12 @@
 package com.google.gerrit.acceptance.testsuite.account;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
 
 @AutoValue
 public abstract class TestAccountUpdate {
@@ -32,11 +36,14 @@
 
   public abstract Optional<Boolean> active();
 
+  public abstract Function<ImmutableSet<String>, Set<String>> secondaryEmailsModification();
+
   abstract ThrowingConsumer<TestAccountUpdate> accountUpdater();
 
   public static Builder builder(ThrowingConsumer<TestAccountUpdate> accountUpdater) {
     return new AutoValue_TestAccountUpdate.Builder()
         .accountUpdater(accountUpdater)
+        .secondaryEmailsModification(in -> in)
         .httpPassword("http-pass");
   }
 
@@ -82,6 +89,37 @@
       return active(false);
     }
 
+    abstract Builder secondaryEmailsModification(
+        Function<ImmutableSet<String>, Set<String>> secondaryEmailsModification);
+
+    abstract Function<ImmutableSet<String>, Set<String>> secondaryEmailsModification();
+
+    public Builder clearSecondaryEmails() {
+      return secondaryEmailsModification(originalSecondaryEmail -> ImmutableSet.of());
+    }
+
+    public Builder addSecondaryEmail(String secondaryEmail) {
+      Function<ImmutableSet<String>, Set<String>> secondaryEmailsModification =
+          secondaryEmailsModification();
+      secondaryEmailsModification(
+          originalSecondaryEmails ->
+              Sets.union(
+                  secondaryEmailsModification.apply(originalSecondaryEmails),
+                  ImmutableSet.of(secondaryEmail)));
+      return this;
+    }
+
+    public Builder removeSecondaryEmail(String secondaryEmail) {
+      Function<ImmutableSet<String>, Set<String>> previousModification =
+          secondaryEmailsModification();
+      secondaryEmailsModification(
+          originalSecondaryEmails ->
+              Sets.difference(
+                  previousModification.apply(originalSecondaryEmails),
+                  ImmutableSet.of(secondaryEmail)));
+      return this;
+    }
+
     abstract Builder accountUpdater(ThrowingConsumer<TestAccountUpdate> accountUpdater);
 
     abstract TestAccountUpdate autoBuild();
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
new file mode 100644
index 0000000..c4e4192
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+
+/**
+ * An aggregation of operations on changes for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+public interface ChangeOperations {
+
+  /**
+   * Starts the fluent chain for querying or modifying a change. Please see the methods of {@link
+   * PerChangeOperations} for details on possible operations.
+   *
+   * @return an aggregation of operations on a specific change
+   */
+  PerChangeOperations change(Change.Id changeId);
+
+  /**
+   * Starts the fluent chain to create a change. The returned builder can be used to specify the
+   * attributes of the new change. To create the change for real, {@link
+   * TestChangeCreation.Builder#create()} must be called.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * Change.Id createdChangeId = changeOperations
+   *     .newChange()
+   *     .file("file1")
+   *     .content("Line 1\nLine2\n")
+   *     .create();
+   * </pre>
+   *
+   * <p><strong>Note:</strong> There must be at least one existing user and repository.
+   *
+   * @return a builder to create the new change
+   */
+  TestChangeCreation.Builder newChange();
+
+  /** An aggregation of methods on a specific change. */
+  interface PerChangeOperations {
+
+    /**
+     * Checks whether the change exists.
+     *
+     * @return {@code true} if the change exists
+     */
+    boolean exists();
+
+    /**
+     * Retrieves the change.
+     *
+     * <p><strong>Note:</strong> This call will fail with an exception if the requested change
+     * doesn't exist. If you want to check for the existence of a change, use {@link #exists()}
+     * instead.
+     *
+     * @return the corresponding {@code TestChange}
+     */
+    TestChange get();
+
+    /**
+     * Starts the fluent chain to create a new patchset. The returned builder can be used to specify
+     * the attributes of the new patchset. To create the patchset for real, {@link
+     * TestPatchsetCreation.Builder#create()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * PatchSet.Id createdPatchsetId = changeOperations
+     *     .change(changeId)
+     *     .newPatchset()
+     *     .file("file1")
+     *     .content("Line 1\nLine2\n")
+     *     .create();
+     * </pre>
+     *
+     * @return builder to create a new patchset
+     */
+    TestPatchsetCreation.Builder newPatchset();
+
+    /**
+     * Starts the fluent chain for querying or modifying a patchset. Please see the methods of
+     * {@link PerPatchsetOperations} for details on possible operations.
+     *
+     * @return an aggregation of operations on a specific patchset
+     */
+    PerPatchsetOperations patchset(PatchSet.Id patchsetId);
+
+    /**
+     * Like {@link #patchset(PatchSet.Id)} but for the current patchset.
+     *
+     * @return an aggregation of operations on a specific patchset
+     */
+    PerPatchsetOperations currentPatchset();
+
+    /**
+     * Starts the fluent chain for querying or modifying a published comment. Please see the methods
+     * of {@link PerCommentOperations} for details on possible operations.
+     *
+     * @return an aggregation of operations on a specific comment
+     */
+    PerCommentOperations comment(String commentUuid);
+
+    /**
+     * Starts the fluent chain for querying or modifying a draft comment. Please see the methods of
+     * {@link PerDraftCommentOperations} for details on possible operations.
+     *
+     * @return an aggregation of operations on a specific draft comment
+     */
+    PerDraftCommentOperations draftComment(String commentUuid);
+
+    /**
+     * Starts the fluent chain for querying or modifying a robot comment. Please see the methods of
+     * {@link PerRobotCommentOperations} for details on possible operations.
+     *
+     * @return an aggregation of operations on a specific robot comment
+     */
+    PerRobotCommentOperations robotComment(String commentUuid);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
new file mode 100644
index 0000000..3b15b57
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -0,0 +1,568 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.edit.tree.TreeCreator;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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.update.BatchUpdate;
+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.inject.Inject;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.ChangeIdUtil;
+
+/**
+ * The implementation of {@link ChangeOperations}.
+ *
+ * <p>There is only one implementation of {@link ChangeOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class ChangeOperationsImpl implements ChangeOperations {
+  private final Sequences seq;
+  private final ChangeInserter.Factory changeInserterFactory;
+  private final PatchSetInserter.Factory patchsetInserterFactory;
+  private final GitRepositoryManager repositoryManager;
+  private final AccountResolver resolver;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final PersonIdent serverIdent;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final ProjectCache projectCache;
+  private final ChangeFinder changeFinder;
+  private final PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory;
+  private final PerCommentOperationsImpl.Factory perCommentOperationsFactory;
+  private final PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory;
+  private final PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory;
+
+  @Inject
+  public ChangeOperationsImpl(
+      Sequences seq,
+      ChangeInserter.Factory changeInserterFactory,
+      PatchSetInserter.Factory patchsetInserterFactory,
+      GitRepositoryManager repositoryManager,
+      AccountResolver resolver,
+      IdentifiedUser.GenericFactory userFactory,
+      @GerritPersonIdent PersonIdent serverIdent,
+      BatchUpdate.Factory batchUpdateFactory,
+      ProjectCache projectCache,
+      ChangeFinder changeFinder,
+      PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory,
+      PerCommentOperationsImpl.Factory perCommentOperationsFactory,
+      PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory,
+      PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory) {
+    this.seq = seq;
+    this.changeInserterFactory = changeInserterFactory;
+    this.patchsetInserterFactory = patchsetInserterFactory;
+    this.repositoryManager = repositoryManager;
+    this.resolver = resolver;
+    this.userFactory = userFactory;
+    this.serverIdent = serverIdent;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.projectCache = projectCache;
+    this.changeFinder = changeFinder;
+    this.perPatchsetOperationsFactory = perPatchsetOperationsFactory;
+    this.perCommentOperationsFactory = perCommentOperationsFactory;
+    this.perDraftCommentOperationsFactory = perDraftCommentOperationsFactory;
+    this.perRobotCommentOperationsFactory = perRobotCommentOperationsFactory;
+  }
+
+  @Override
+  public PerChangeOperations change(Change.Id changeId) {
+    return new PerChangeOperationsImpl(changeId);
+  }
+
+  @Override
+  public TestChangeCreation.Builder newChange() {
+    return TestChangeCreation.builder(this::createChange);
+  }
+
+  private Change.Id createChange(TestChangeCreation changeCreation) throws Exception {
+    Change.Id changeId = Change.id(seq.nextChangeId());
+    Project.NameKey project = getTargetProject(changeCreation);
+
+    try (Repository repository = repositoryManager.openRepository(project);
+        ObjectInserter objectInserter = repository.newObjectInserter();
+        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+      Timestamp now = TimeUtil.nowTs();
+      IdentifiedUser changeOwner = getChangeOwner(changeCreation);
+      PersonIdent authorAndCommitter =
+          changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
+      ObjectId commitId =
+          createCommit(repository, revWalk, objectInserter, changeCreation, authorAndCommitter);
+
+      String refName = RefNames.fullName(changeCreation.branch());
+      ChangeInserter inserter = getChangeInserter(changeId, refName, commitId);
+
+      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
+        batchUpdate.setRepository(repository, revWalk, objectInserter);
+        batchUpdate.insertChange(inserter);
+        batchUpdate.execute();
+      }
+      return changeId;
+    }
+  }
+
+  private Project.NameKey getTargetProject(TestChangeCreation changeCreation) {
+    if (changeCreation.project().isPresent()) {
+      return changeCreation.project().get();
+    }
+
+    return getArbitraryProject();
+  }
+
+  private Project.NameKey getArbitraryProject() {
+    Project.NameKey allProjectsName = projectCache.getAllProjects().getNameKey();
+    Project.NameKey allUsersName = projectCache.getAllUsers().getNameKey();
+    Optional<Project.NameKey> arbitraryProject =
+        projectCache.all().stream()
+            .filter(
+                name ->
+                    !Objects.equals(name, allProjectsName) && !Objects.equals(name, allUsersName))
+            .findFirst();
+    checkState(
+        arbitraryProject.isPresent(),
+        "At least one repository must be available on the Gerrit server");
+    return arbitraryProject.get();
+  }
+
+  private IdentifiedUser getChangeOwner(TestChangeCreation changeCreation)
+      throws IOException, ConfigInvalidException {
+    if (changeCreation.owner().isPresent()) {
+      return userFactory.create(changeCreation.owner().get());
+    }
+
+    return getArbitraryUser();
+  }
+
+  private IdentifiedUser getArbitraryUser() throws ConfigInvalidException, IOException {
+    ImmutableSet<Account.Id> foundAccounts = resolver.resolveIgnoreVisibility("").asIdSet();
+    checkState(
+        !foundAccounts.isEmpty(),
+        "At least one user account must be available on the Gerrit server");
+    return userFactory.create(foundAccounts.iterator().next());
+  }
+
+  private ObjectId createCommit(
+      Repository repository,
+      RevWalk revWalk,
+      ObjectInserter objectInserter,
+      TestChangeCreation changeCreation,
+      PersonIdent authorAndCommitter)
+      throws IOException, BadRequestException {
+    ImmutableList<ObjectId> parentCommits = getParentCommits(repository, revWalk, changeCreation);
+    TreeCreator treeCreator =
+        getTreeCreator(objectInserter, parentCommits, changeCreation.mergeStrategy());
+    ObjectId tree = createNewTree(repository, treeCreator, changeCreation.treeModifications());
+    String commitMessage = correctCommitMessage(changeCreation.commitMessage());
+    return createCommit(
+        objectInserter, tree, parentCommits, authorAndCommitter, authorAndCommitter, commitMessage);
+  }
+
+  private ImmutableList<ObjectId> getParentCommits(
+      Repository repository, RevWalk revWalk, TestChangeCreation changeCreation) {
+
+    return changeCreation
+        .parents()
+        .map(parents -> resolveParents(repository, revWalk, parents))
+        .orElseGet(() -> asImmutableList(getTip(repository, changeCreation.branch())));
+  }
+
+  private ImmutableList<ObjectId> resolveParents(
+      Repository repository, RevWalk revWalk, ImmutableList<TestCommitIdentifier> parents) {
+    return parents.stream()
+        .map(parent -> resolveCommit(repository, revWalk, parent))
+        .collect(toImmutableList());
+  }
+
+  private ObjectId resolveCommit(
+      Repository repository, RevWalk revWalk, TestCommitIdentifier parentCommit) {
+    switch (parentCommit.getKind()) {
+      case BRANCH:
+        return resolveBranchTip(repository, parentCommit.branch());
+      case CHANGE_ID:
+        return resolveChange(parentCommit.changeId());
+      case COMMIT_SHA_1:
+        return resolveCommitFromSha1(revWalk, parentCommit.commitSha1());
+      case PATCHSET_ID:
+        return resolvePatchset(parentCommit.patchsetId());
+      default:
+        throw new IllegalStateException(
+            String.format("No parent behavior implemented for %s.", parentCommit.getKind()));
+    }
+  }
+
+  private static ObjectId resolveBranchTip(Repository repository, String branchName) {
+    return getTip(repository, branchName)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Tip of branch %s not found and hence can't be used as parent.",
+                        branchName)));
+  }
+
+  private static Optional<ObjectId> getTip(Repository repository, String branch) {
+    try {
+      Optional<Ref> ref = Optional.ofNullable(repository.findRef(branch));
+      return ref.map(Ref::getObjectId);
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
+  }
+
+  private ObjectId resolveChange(Change.Id changeId) {
+    Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
+    return changeNotes
+        .map(ChangeNotes::getCurrentPatchSet)
+        .map(PatchSet::commitId)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Change %s not found and hence can't be used as parent.", changeId)));
+  }
+
+  private static RevCommit resolveCommitFromSha1(RevWalk revWalk, ObjectId commitSha1) {
+    try {
+      return revWalk.parseCommit(commitSha1);
+    } catch (Exception e) {
+      throw new IllegalStateException(
+          String.format("Commit %s not found and hence can't be used as parent/base.", commitSha1),
+          e);
+    }
+  }
+
+  private ObjectId resolvePatchset(PatchSet.Id patchsetId) {
+    Optional<ChangeNotes> changeNotes = changeFinder.findOne(patchsetId.changeId());
+    return changeNotes
+        .map(ChangeNotes::getPatchSets)
+        .map(patchsets -> patchsets.get(patchsetId))
+        .map(PatchSet::commitId)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Patchset %s not found and hence can't be used as parent.", patchsetId)));
+  }
+
+  private static <T> ImmutableList<T> asImmutableList(Optional<T> value) {
+    return Streams.stream(value).collect(toImmutableList());
+  }
+
+  private static TreeCreator getTreeCreator(
+      RevWalk revWalk, ObjectId customBaseCommit, ImmutableList<ObjectId> parentCommits) {
+    RevCommit commit = resolveCommitFromSha1(revWalk, customBaseCommit);
+    // Use actual parents; relevant for example when a file is restored (->
+    // RestoreFileModification).
+    return TreeCreator.basedOnTree(commit.getTree(), parentCommits);
+  }
+
+  private static TreeCreator getTreeCreator(
+      ObjectInserter objectInserter,
+      ImmutableList<ObjectId> parentCommits,
+      MergeStrategy mergeStrategy) {
+    if (parentCommits.isEmpty()) {
+      return TreeCreator.basedOnEmptyTree();
+    }
+    ObjectId baseTreeId = merge(objectInserter, parentCommits, mergeStrategy);
+    return TreeCreator.basedOnTree(baseTreeId, parentCommits);
+  }
+
+  private static ObjectId merge(
+      ObjectInserter objectInserter,
+      ImmutableList<ObjectId> parentCommits,
+      MergeStrategy mergeStrategy) {
+    try {
+      Merger merger = mergeStrategy.newMerger(objectInserter, new Config());
+      boolean mergeSuccessful = merger.merge(parentCommits.toArray(new AnyObjectId[0]));
+      if (!mergeSuccessful) {
+        throw new IllegalStateException(
+            "Conflicts encountered while merging the specified parents. Use"
+                + " mergeOfButBaseOnFirst() instead to avoid these conflicts and define any"
+                + " other desired file contents with file().content().");
+      }
+      return merger.getResultTreeId();
+    } catch (IOException e) {
+      throw new IllegalStateException(
+          "Creating the merge commits of the specified parents failed for an unknown reason.", e);
+    }
+  }
+
+  private static ObjectId createNewTree(
+      Repository repository,
+      TreeCreator treeCreator,
+      ImmutableList<TreeModification> treeModifications)
+      throws IOException {
+    treeCreator.addTreeModifications(treeModifications);
+    return treeCreator.createNewTreeAndGetId(repository);
+  }
+
+  private String correctCommitMessage(String desiredCommitMessage) throws BadRequestException {
+    String commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(desiredCommitMessage);
+
+    if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
+      ObjectId id = CommitMessageUtil.generateChangeId();
+      commitMessage = ChangeIdUtil.insertId(commitMessage, id);
+    }
+
+    return commitMessage;
+  }
+
+  private ObjectId createCommit(
+      ObjectInserter objectInserter,
+      ObjectId tree,
+      ImmutableList<ObjectId> parentCommitIds,
+      PersonIdent author,
+      PersonIdent committer,
+      String commitMessage)
+      throws IOException {
+    CommitBuilder builder = new CommitBuilder();
+    builder.setTreeId(tree);
+    builder.setParentIds(parentCommitIds);
+    builder.setAuthor(author);
+    builder.setCommitter(committer);
+    builder.setMessage(commitMessage);
+    ObjectId newCommitId = objectInserter.insert(builder);
+    objectInserter.flush();
+    return newCommitId;
+  }
+
+  private ChangeInserter getChangeInserter(Change.Id changeId, String refName, ObjectId commitId) {
+    ChangeInserter inserter = changeInserterFactory.create(changeId, commitId, refName);
+    inserter.setMessage(String.format("Uploaded patchset %d.", inserter.getPatchSetId().get()));
+    return inserter;
+  }
+
+  private class PerChangeOperationsImpl implements PerChangeOperations {
+
+    private final Change.Id changeId;
+
+    public PerChangeOperationsImpl(Change.Id changeId) {
+      this.changeId = changeId;
+    }
+
+    @Override
+    public boolean exists() {
+      return changeFinder.findOne(changeId).isPresent();
+    }
+
+    @Override
+    public TestChange get() {
+      return toTestChange(getChangeNotes().getChange());
+    }
+
+    private ChangeNotes getChangeNotes() {
+      Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
+      checkState(changeNotes.isPresent(), "Tried to get non-existing test change.");
+      return changeNotes.get();
+    }
+
+    private TestChange toTestChange(Change change) {
+      return TestChange.builder()
+          .numericChangeId(change.getId())
+          .changeId(change.getKey().get())
+          .build();
+    }
+
+    @Override
+    public TestPatchsetCreation.Builder newPatchset() {
+      return TestPatchsetCreation.builder(this::createPatchset);
+    }
+
+    private PatchSet.Id createPatchset(TestPatchsetCreation patchsetCreation)
+        throws IOException, RestApiException, UpdateException {
+      ChangeNotes changeNotes = getChangeNotes();
+      Project.NameKey project = changeNotes.getProjectName();
+      try (Repository repository = repositoryManager.openRepository(project);
+          ObjectInserter objectInserter = repository.newObjectInserter();
+          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+        Timestamp now = TimeUtil.nowTs();
+        ObjectId newPatchsetCommit =
+            createPatchsetCommit(
+                repository, revWalk, objectInserter, changeNotes, patchsetCreation, now);
+
+        PatchSet.Id patchsetId =
+            ChangeUtil.nextPatchSetId(repository, changeNotes.getCurrentPatchSet().id());
+        PatchSetInserter patchSetInserter =
+            getPatchSetInserter(changeNotes, newPatchsetCommit, patchsetId);
+
+        IdentifiedUser changeOwner = userFactory.create(changeNotes.getChange().getOwner());
+        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
+          batchUpdate.setRepository(repository, revWalk, objectInserter);
+          batchUpdate.addOp(changeId, patchSetInserter);
+          batchUpdate.execute();
+        }
+        return patchsetId;
+      }
+    }
+
+    private ObjectId createPatchsetCommit(
+        Repository repository,
+        RevWalk revWalk,
+        ObjectInserter objectInserter,
+        ChangeNotes changeNotes,
+        TestPatchsetCreation patchsetCreation,
+        Timestamp now)
+        throws IOException, BadRequestException {
+      ObjectId oldPatchsetCommitId = changeNotes.getCurrentPatchSet().commitId();
+      RevCommit oldPatchsetCommit = repository.parseCommit(oldPatchsetCommitId);
+
+      ImmutableList<ObjectId> parentCommitIds =
+          getParents(repository, revWalk, patchsetCreation, oldPatchsetCommit);
+      TreeCreator treeCreator = getTreeCreator(revWalk, oldPatchsetCommit, parentCommitIds);
+      ObjectId tree = createNewTree(repository, treeCreator, patchsetCreation.treeModifications());
+
+      String commitMessage =
+          correctCommitMessage(
+              changeNotes.getChange().getKey().get(),
+              patchsetCreation.commitMessage().orElseGet(oldPatchsetCommit::getFullMessage));
+
+      PersonIdent author = getAuthor(oldPatchsetCommit);
+      PersonIdent committer = getCommitter(oldPatchsetCommit, now);
+      return createCommit(objectInserter, tree, parentCommitIds, author, committer, commitMessage);
+    }
+
+    private String correctCommitMessage(String oldChangeId, String desiredCommitMessage)
+        throws BadRequestException {
+      String commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(desiredCommitMessage);
+
+      // Remove initial 'I' and treat the rest as ObjectId. This is not the cleanest approach but
+      // unfortunately, we don't seem to have other utility code which takes the string-based
+      // change-id and ensures that it is part of the commit message.
+      ObjectId id = ObjectId.fromString(oldChangeId.substring(1));
+      commitMessage = ChangeIdUtil.insertId(commitMessage, id, false);
+
+      return commitMessage;
+    }
+
+    private PersonIdent getAuthor(RevCommit oldPatchsetCommit) {
+      return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent);
+    }
+
+    private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Timestamp now) {
+      PersonIdent oldPatchsetCommitter =
+          Optional.ofNullable(oldPatchsetCommit.getCommitterIdent()).orElse(serverIdent);
+      if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen())) {
+        /* We need to ensure that the resulting commit SHA-1 is different from the old patchset.
+         * In real situations, this automatically happens as two patchsets won't have exactly the
+         * same commit timestamp even when the tree and commit message are the same. In tests,
+         * we can easily end up with the same timestamp as Git uses second precision for timestamps.
+         * We could of course require that tests must use TestTimeUtil#setClockStep but
+         * that would be an unnecessary nuisance for test writers. Hence, go with a simple solution
+         * here and simply add a second. */
+        now = Timestamp.from(now.toInstant().plusSeconds(1));
+      }
+      return new PersonIdent(oldPatchsetCommitter, now);
+    }
+
+    private long asSeconds(Date date) {
+      return date.getTime() / 1000;
+    }
+
+    private ImmutableList<ObjectId> getParents(
+        Repository repository,
+        RevWalk revWalk,
+        TestPatchsetCreation patchsetCreation,
+        RevCommit oldPatchsetCommit) {
+      return patchsetCreation
+          .parents()
+          .map(parents -> resolveParents(repository, revWalk, parents))
+          .orElseGet(
+              () -> Arrays.stream(oldPatchsetCommit.getParents()).collect(toImmutableList()));
+    }
+
+    private PatchSetInserter getPatchSetInserter(
+        ChangeNotes changeNotes, ObjectId newPatchsetCommit, PatchSet.Id patchsetId) {
+      PatchSetInserter patchSetInserter =
+          patchsetInserterFactory.create(changeNotes, patchsetId, newPatchsetCommit);
+      patchSetInserter.setCheckAddPatchSetPermission(false);
+      patchSetInserter.setMessage(String.format("Uploaded patchset %d.", patchsetId.get()));
+      return patchSetInserter;
+    }
+
+    @Override
+    public PerPatchsetOperations patchset(PatchSet.Id patchsetId) {
+      return perPatchsetOperationsFactory.create(getChangeNotes(), patchsetId);
+    }
+
+    @Override
+    public PerPatchsetOperations currentPatchset() {
+      ChangeNotes changeNotes = getChangeNotes();
+      return perPatchsetOperationsFactory.create(
+          changeNotes, changeNotes.getChange().currentPatchSetId());
+    }
+
+    @Override
+    public PerCommentOperations comment(String commentUuid) {
+      ChangeNotes changeNotes = getChangeNotes();
+      return perCommentOperationsFactory.create(changeNotes, commentUuid);
+    }
+
+    @Override
+    public PerDraftCommentOperations draftComment(String commentUuid) {
+      ChangeNotes changeNotes = getChangeNotes();
+      return perDraftCommentOperationsFactory.create(changeNotes, commentUuid);
+    }
+
+    @Override
+    public PerRobotCommentOperations robotComment(String commentUuid) {
+      ChangeNotes changeNotes = getChangeNotes();
+      return perRobotCommentOperationsFactory.create(changeNotes, commentUuid);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java b/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java
new file mode 100644
index 0000000..b7e720b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+/**
+ * Marks the commit that contains the comment (also known as side). Used by {@link
+ * TestCommentCreation} and {@link TestRobotCommentCreation}.
+ */
+enum CommentSide {
+  PATCHSET_COMMIT(1),
+  AUTO_MERGE_COMMIT(0),
+  PARENT_COMMIT(-1),
+  SECOND_PARENT_COMMIT(-2);
+
+  private final short numericSide;
+
+  CommentSide(int numericSide) {
+    this.numericSide = (short) numericSide;
+  }
+
+  public short getNumericSide() {
+    return numericSide;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java
new file mode 100644
index 0000000..c8514a7
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import java.util.function.Function;
+
+/**
+ * Builder for the file specification of line/range comments. Used by {@link TestCommentCreation}
+ * and {@link TestRobotCommentCreation}.
+ */
+public class FileBuilder<T> {
+  private final Function<String, T> nextStepProvider;
+
+  public FileBuilder(Function<String, T> nextStepProvider) {
+    this.nextStepProvider = nextStepProvider;
+  }
+  /** File on which the comment should be added. */
+  public T ofFile(String file) {
+    return nextStepProvider.apply(file);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
new file mode 100644
index 0000000..d0ccd5b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
+import com.google.gerrit.server.edit.tree.DeleteFileModification;
+import com.google.gerrit.server.edit.tree.RenameFileModification;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import java.util.function.Consumer;
+
+/** Builder to simplify file content specification. */
+public class FileContentBuilder<T> {
+  private final T builder;
+  private final String filePath;
+  private final Consumer<TreeModification> modificationToBuilderAdder;
+
+  FileContentBuilder(
+      T builder, String filePath, Consumer<TreeModification> modificationToBuilderAdder) {
+    checkNotNull(Strings.emptyToNull(filePath), "File path must not be null or empty.");
+    this.builder = builder;
+    this.filePath = filePath;
+    this.modificationToBuilderAdder = modificationToBuilderAdder;
+  }
+
+  /** Content of the file. Must not be empty. */
+  public T content(String content) {
+    checkNotNull(
+        Strings.emptyToNull(content),
+        "Empty file content is not supported. Adjust test API if necessary.");
+    modificationToBuilderAdder.accept(
+        new ChangeFileContentModification(filePath, RawInputUtil.create(content)));
+    return builder;
+  }
+
+  /** Deletes the file. */
+  public T delete() {
+    modificationToBuilderAdder.accept(new DeleteFileModification(filePath));
+    return builder;
+  }
+
+  /**
+   * Renames the file while keeping its content.
+   *
+   * <p>If you want to both rename the file and adjust its content, delete the old path via {@link
+   * #delete()} and provide the desired content for the new path via {@link #content(String)}. If
+   * you use that approach, make sure to use a new content which is similar enough to the old (at
+   * least 60% line similarity) as otherwise Gerrit/Git won't identify it as a rename.
+   *
+   * <p>To create copied files, you need to go even one step further. Also rename the file you copy
+   * at the same time (-> delete old path + add two paths with the old content)! If you also want to
+   * adjust the content of the copy, you need to also slightly modify the content of the renamed
+   * file. Adjust the content of the copy slightly more if you want to control which file ends up as
+   * copy and which as rename (but keep the 60% line similarity threshold in mind).
+   *
+   * @param newFilePath new path of the file
+   */
+  public T renameTo(String newFilePath) {
+    modificationToBuilderAdder.accept(new RenameFileModification(filePath, newFilePath));
+    return builder;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/MultipleParentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/MultipleParentBuilder.java
new file mode 100644
index 0000000..63d8c0a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/MultipleParentBuilder.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.common.collect.ImmutableList;
+import java.util.function.Function;
+
+/** Builder to simplify specifying multiple parents for a change. */
+public class MultipleParentBuilder<T> {
+  private final Function<ImmutableList<TestCommitIdentifier>, T> parentsToBuilderAdder;
+  private final ImmutableList.Builder<TestCommitIdentifier> parents;
+
+  public MultipleParentBuilder(
+      Function<ImmutableList<TestCommitIdentifier>, T> parentsToBuilderAdder,
+      TestCommitIdentifier firstParent) {
+    this.parentsToBuilderAdder = parentsToBuilderAdder;
+    parents = ImmutableList.builder();
+    parents.add(firstParent);
+  }
+
+  /** Adds an intermediate parent. */
+  public ParentBuilder<MultipleParentBuilder<T>> followedBy() {
+    return new ParentBuilder<>(
+        parent -> {
+          parents.add(parent);
+          return this;
+        });
+  }
+
+  /** Adds the last parent. */
+  public ParentBuilder<T> and() {
+    return new ParentBuilder<>(
+        (parent) -> parentsToBuilderAdder.apply(parents.add(parent).build()));
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java
new file mode 100644
index 0000000..b57aa6d
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import java.util.function.Function;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Builder to simplify parent specification of a change. */
+public class ParentBuilder<T> {
+  private final Function<TestCommitIdentifier, T> parentToBuilderAdder;
+
+  public ParentBuilder(Function<TestCommitIdentifier, T> parentToBuilderAdder) {
+    this.parentToBuilderAdder = parentToBuilderAdder;
+  }
+
+  /** Use the commit identified by the specified SHA-1. */
+  public T commit(ObjectId commitSha1) {
+    return parentToBuilderAdder.apply(TestCommitIdentifier.ofCommitSha1(commitSha1));
+  }
+
+  /**
+   * Use the commit which is at the tip of the specified branch. Short branch names (without
+   * refs/heads) are automatically expanded.
+   */
+  public T tipOfBranch(String branchName) {
+    return parentToBuilderAdder.apply(TestCommitIdentifier.ofBranch(branchName));
+  }
+
+  /** Use the current patchset commit of the indicated change. */
+  public T change(Change.Id changeId) {
+    return parentToBuilderAdder.apply(TestCommitIdentifier.ofChangeId(changeId));
+  }
+
+  /** Use the commit identified by the specified patchset. */
+  public T patchset(PatchSet.Id patchsetId) {
+    return parentToBuilderAdder.apply(TestCommitIdentifier.ofPatchsetId(patchsetId));
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperations.java
new file mode 100644
index 0000000..aa2827c
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperations.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+/** An aggregation of methods on a specific, published comment. */
+public interface PerCommentOperations {
+
+  /**
+   * Retrieves the published comment.
+   *
+   * <p><strong>Note:</strong> This call will fail with an exception if the requested comment
+   * doesn't exist or if it is a comment of another type.
+   *
+   * @return the corresponding {@code TestComment}
+   */
+  TestHumanComment get();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java
new file mode 100644
index 0000000..0218731
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.collect.MoreCollectors.onlyElement;
+
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * The implementation of {@link PerCommentOperations}.
+ *
+ * <p>There is only one implementation of {@link PerCommentOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class PerCommentOperationsImpl implements PerCommentOperations {
+  private final CommentsUtil commentsUtil;
+
+  private final ChangeNotes changeNotes;
+  private final String commentUuid;
+
+  public interface Factory {
+    PerCommentOperationsImpl create(ChangeNotes changeNotes, String commentUuid);
+  }
+
+  @Inject
+  public PerCommentOperationsImpl(
+      CommentsUtil commentsUtil, @Assisted ChangeNotes changeNotes, @Assisted String commentUuid) {
+    this.commentsUtil = commentsUtil;
+    this.changeNotes = changeNotes;
+    this.commentUuid = commentUuid;
+  }
+
+  @Override
+  public TestHumanComment get() {
+    HumanComment comment =
+        commentsUtil.publishedHumanCommentsByChange(changeNotes).stream()
+            .filter(foundComment -> foundComment.key.uuid.equals(commentUuid))
+            .collect(onlyElement());
+    return toTestComment(comment);
+  }
+
+  static TestHumanComment toTestComment(HumanComment comment) {
+    return TestHumanComment.builder()
+        .uuid(comment.key.uuid)
+        .parentUuid(comment.parentUuid)
+        .tag(comment.tag)
+        .unresolved(comment.unresolved)
+        .build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperations.java
new file mode 100644
index 0000000..cc1e844
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperations.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+/** An aggregation of methods on a specific draft comment. */
+public interface PerDraftCommentOperations {
+
+  /**
+   * Retrieves the draft comment.
+   *
+   * <p><strong>Note:</strong> This call will fail with an exception if the requested comment
+   * doesn't exist or if it is a comment of another type.
+   *
+   * @return the corresponding {@code TestComment}
+   */
+  TestHumanComment get();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java
new file mode 100644
index 0000000..db264c5
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.collect.MoreCollectors.onlyElement;
+import static com.google.gerrit.acceptance.testsuite.change.PerCommentOperationsImpl.toTestComment;
+
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * The implementation of {@link PerDraftCommentOperationsImpl}.
+ *
+ * <p>There is only one implementation of {@link PerDraftCommentOperations}. Nevertheless, we keep
+ * the separation between interface and implementation to enhance clarity.
+ */
+public class PerDraftCommentOperationsImpl implements PerDraftCommentOperations {
+  private final CommentsUtil commentsUtil;
+
+  private final ChangeNotes changeNotes;
+  private final String commentUuid;
+
+  public interface Factory {
+    PerDraftCommentOperationsImpl create(ChangeNotes changeNotes, String commentUuid);
+  }
+
+  @Inject
+  public PerDraftCommentOperationsImpl(
+      CommentsUtil commentsUtil, @Assisted ChangeNotes changeNotes, @Assisted String commentUuid) {
+    this.commentsUtil = commentsUtil;
+    this.changeNotes = changeNotes;
+    this.commentUuid = commentUuid;
+  }
+
+  @Override
+  public TestHumanComment get() {
+    HumanComment comment =
+        commentsUtil.draftByChange(changeNotes).stream()
+            .filter(foundComment -> foundComment.key.uuid.equals(commentUuid))
+            .collect(onlyElement());
+    return toTestComment(comment);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
new file mode 100644
index 0000000..f4c70bd
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.acceptance.testsuite.change;
+
+/** An aggregation of methods on a specific patchset. */
+public interface PerPatchsetOperations {
+
+  /**
+   * Retrieves the patchset.
+   *
+   * <p><strong>Note:</strong> This call will fail with an exception if the requested patchset
+   * doesn't exist.
+   *
+   * @return the corresponding {@code TestPatchset}
+   */
+  TestPatchset get();
+
+  /**
+   * Starts the fluent chain to create a new, published comment. The returned builder can be used to
+   * specify the attributes of the comment. To create the comment for real, {@link
+   * TestCommentCreation.Builder#create()} must be called.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * String createdCommentUuid = changeOperations
+   *     .change(changeId)
+   *     .currentPatchset()
+   *     .newComment()
+   *     .onLine(2)
+   *     .ofFile("file1")
+   *     .create();
+   * </pre>
+   *
+   * @return builder to create a new comment
+   */
+  TestCommentCreation.Builder newComment();
+
+  /**
+   * Starts the fluent chain to create a new draft comment. The returned builder can be used to
+   * specify the attributes of the draft comment. To create the draft comment for real, {@link
+   * TestCommentCreation.Builder#create()} must be called.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * String createdDraftCommentUuid = changeOperations
+   *     .change(changeId)
+   *     .currentPatchset()
+   *     .newDraftComment()
+   *     .onLine(2)
+   *     .ofFile("file1")
+   *     .create();
+   * </pre>
+   *
+   * @return builder to create a new comment
+   */
+  TestCommentCreation.Builder newDraftComment();
+
+  /**
+   * Starts the fluent chain to create a new robot comment. The returned builder can be used to
+   * specify the attributes of the robot comment. To create the robot comment for real, {@link
+   * TestRobotCommentCreation.Builder#create()} must be called.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * String createdRobotCommentUuid = changeOperations
+   *     .change(changeId)
+   *     .currentPatchset()
+   *     .newRobotComment()
+   *     .onLine(2)
+   *     .ofFile("file1")
+   *     .create();
+   * </pre>
+   *
+   * @return builder to create a new comment
+   */
+  TestRobotCommentCreation.Builder newRobotComment();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
new file mode 100644
index 0000000..eda6c7e
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment.Status;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RobotComment;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Comment.Range;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * The implementation of {@link PerPatchsetOperations}.
+ *
+ * <p>There is only one implementation of {@link PerPatchsetOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
+  private final GitRepositoryManager repositoryManager;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final CommentsUtil commentsUtil;
+
+  private final ChangeNotes changeNotes;
+  private final PatchSet.Id patchsetId;
+
+  public interface Factory {
+    PerPatchsetOperationsImpl create(ChangeNotes changeNotes, PatchSet.Id patchsetId);
+  }
+
+  @Inject
+  private PerPatchsetOperationsImpl(
+      GitRepositoryManager repositoryManager,
+      GenericFactory userFactory,
+      BatchUpdate.Factory batchUpdateFactory,
+      CommentsUtil commentsUtil,
+      @Assisted ChangeNotes changeNotes,
+      @Assisted PatchSet.Id patchsetId) {
+    this.repositoryManager = repositoryManager;
+    this.userFactory = userFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.commentsUtil = commentsUtil;
+    this.changeNotes = changeNotes;
+    this.patchsetId = patchsetId;
+  }
+
+  @Override
+  public TestPatchset get() {
+    PatchSet patchset = changeNotes.getPatchSets().get(patchsetId);
+    return TestPatchset.builder().patchsetId(patchsetId).commitId(patchset.commitId()).build();
+  }
+
+  @Override
+  public TestCommentCreation.Builder newComment() {
+    return TestCommentCreation.builder(this::createComment, Status.PUBLISHED);
+  }
+
+  @Override
+  public TestCommentCreation.Builder newDraftComment() {
+    return TestCommentCreation.builder(this::createComment, Status.DRAFT);
+  }
+
+  @Override
+  public TestRobotCommentCreation.Builder newRobotComment() {
+    return TestRobotCommentCreation.builder(this::createRobotComment);
+  }
+
+  private String createComment(TestCommentCreation commentCreation)
+      throws IOException, RestApiException, UpdateException {
+    Project.NameKey project = changeNotes.getProjectName();
+
+    try (Repository repository = repositoryManager.openRepository(project);
+        ObjectInserter objectInserter = repository.newObjectInserter();
+        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+      Timestamp now = TimeUtil.nowTs();
+
+      IdentifiedUser author = getAuthor(commentCreation);
+      CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
+      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
+        batchUpdate.setRepository(repository, revWalk, objectInserter);
+        batchUpdate.addOp(changeNotes.getChangeId(), commentAdditionOp);
+        batchUpdate.execute();
+      }
+      return commentAdditionOp.createdCommentUuid;
+    }
+  }
+
+  private IdentifiedUser getAuthor(TestCommentCreation commentCreation) {
+    Account.Id authorId = commentCreation.author().orElse(changeNotes.getChange().getOwner());
+    return userFactory.create(authorId);
+  }
+
+  private IdentifiedUser getAuthor(TestRobotCommentCreation robotCommentCreation) {
+    Account.Id authorId = robotCommentCreation.author().orElse(changeNotes.getChange().getOwner());
+    return userFactory.create(authorId);
+  }
+
+  private static Comment.Range toCommentRange(TestRange range) {
+    Comment.Range commentRange = new Range();
+    commentRange.startLine = range.start().line();
+    commentRange.startCharacter = range.start().charOffset();
+    commentRange.endLine = range.end().line();
+    commentRange.endCharacter = range.end().charOffset();
+    return commentRange;
+  }
+
+  private class CommentAdditionOp implements BatchUpdateOp {
+    private String createdCommentUuid;
+    private final TestCommentCreation commentCreation;
+
+    public CommentAdditionOp(TestCommentCreation commentCreation) {
+      this.commentCreation = commentCreation;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext context) {
+      HumanComment comment = toNewComment(context, commentCreation);
+      ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
+      changeUpdate.putComment(commentCreation.status(), comment);
+      // For published comments, only the tag set on the ChangeUpdate (and not on the HumanComment)
+      // matters.
+      commentCreation.tag().ifPresent(changeUpdate::setTag);
+      createdCommentUuid = comment.key.uuid;
+      return true;
+    }
+
+    private HumanComment toNewComment(ChangeContext context, TestCommentCreation commentCreation) {
+      String message = commentCreation.message().orElse("The text of a test comment.");
+
+      String filePath = commentCreation.file().orElse(Patch.PATCHSET_LEVEL);
+      short side = commentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide();
+      Boolean unresolved = commentCreation.unresolved().orElse(null);
+      String parentUuid = commentCreation.parentUuid().orElse(null);
+      Timestamp createdOn =
+          commentCreation.createdOn().map(Timestamp::from).orElse(context.getWhen());
+      HumanComment newComment =
+          commentsUtil.newHumanComment(
+              context.getNotes(),
+              context.getUser(),
+              createdOn,
+              filePath,
+              patchsetId,
+              side,
+              message,
+              unresolved,
+              parentUuid);
+      // For draft comments, only the tag set on the HumanComment (and not on the ChangeUpdate)
+      // matters.
+      commentCreation.tag().ifPresent(tag -> newComment.tag = tag);
+
+      commentCreation.line().ifPresent(line -> newComment.setLineNbrAndRange(line, null));
+      // Specification of range trumps explicit line specification.
+      commentCreation
+          .range()
+          .map(PerPatchsetOperationsImpl::toCommentRange)
+          .ifPresent(range -> newComment.setLineNbrAndRange(null, range));
+
+      commentsUtil.setCommentCommitId(
+          newComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId));
+      return newComment;
+    }
+  }
+
+  private String createRobotComment(TestRobotCommentCreation robotCommentCreation)
+      throws IOException, RestApiException, UpdateException {
+    Project.NameKey project = changeNotes.getProjectName();
+
+    try (Repository repository = repositoryManager.openRepository(project);
+        ObjectInserter objectInserter = repository.newObjectInserter();
+        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+      Timestamp now = TimeUtil.nowTs();
+
+      IdentifiedUser author = getAuthor(robotCommentCreation);
+      RobotCommentAdditionOp robotCommentAdditionOp =
+          new RobotCommentAdditionOp(robotCommentCreation);
+      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
+        batchUpdate.setRepository(repository, revWalk, objectInserter);
+        batchUpdate.addOp(changeNotes.getChangeId(), robotCommentAdditionOp);
+        batchUpdate.execute();
+      }
+      return robotCommentAdditionOp.createdRobotCommentUuid;
+    }
+  }
+
+  private class RobotCommentAdditionOp implements BatchUpdateOp {
+    private String createdRobotCommentUuid;
+    private final TestRobotCommentCreation robotCommentCreation;
+
+    public RobotCommentAdditionOp(TestRobotCommentCreation robotCommentCreation) {
+      this.robotCommentCreation = robotCommentCreation;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext context) {
+      RobotComment robotComment = toNewRobotComment(context, robotCommentCreation);
+      ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
+      changeUpdate.putRobotComment(robotComment);
+      // For robot comments, only the tag set on the ChangeUpdate (and not on the RobotComment)
+      // matters.
+      robotCommentCreation.tag().ifPresent(changeUpdate::setTag);
+      createdRobotCommentUuid = robotComment.key.uuid;
+      return true;
+    }
+
+    private RobotComment toNewRobotComment(
+        ChangeContext context, TestRobotCommentCreation robotCommentCreation) {
+      String message = robotCommentCreation.message().orElse("The text of a test robot comment.");
+
+      String filePath = robotCommentCreation.file().orElse(Patch.PATCHSET_LEVEL);
+      short side = robotCommentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide();
+      String robotId = robotCommentCreation.robotId().orElse("robot");
+      String robotRunId = robotCommentCreation.robotId().orElse("1");
+      RobotComment newRobotComment =
+          commentsUtil.newRobotComment(
+              context, filePath, patchsetId, side, message, robotId, robotRunId);
+
+      // TODO(paiking): This should not be needed, as the tag only matters in ChangeUpdate.
+      robotCommentCreation.tag().ifPresent(tag -> newRobotComment.tag = tag);
+
+      robotCommentCreation.line().ifPresent(line -> newRobotComment.setLineNbrAndRange(line, null));
+      // Specification of range trumps explicit line specification.
+      robotCommentCreation
+          .range()
+          .map(PerPatchsetOperationsImpl::toCommentRange)
+          .ifPresent(range -> newRobotComment.setLineNbrAndRange(null, range));
+
+      robotCommentCreation
+          .parentUuid()
+          .ifPresent(parentUuid -> newRobotComment.parentUuid = parentUuid);
+      robotCommentCreation.url().ifPresent(url -> newRobotComment.url = url);
+      if (!robotCommentCreation.properties().isEmpty()) {
+        newRobotComment.properties = robotCommentCreation.properties();
+      }
+
+      commentsUtil.setCommentCommitId(
+          newRobotComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId));
+      return newRobotComment;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java
new file mode 100644
index 0000000..c9718aa
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+/** An aggregation of methods on a specific, robot comment. */
+public interface PerRobotCommentOperations {
+
+  /**
+   * Retrieves the robot comment.
+   *
+   * <p><strong>Note:</strong> This call will fail with an exception if the requested comment
+   * doesn't exist or if it is a comment of another type.
+   *
+   * @return the corresponding {@code TestRobotComment}
+   */
+  TestRobotComment get();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java
new file mode 100644
index 0000000..075c451
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.collect.MoreCollectors.onlyElement;
+
+import com.google.gerrit.entities.RobotComment;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * The implementation of {@link PerRobotCommentOperations}.
+ *
+ * <p>There is only one implementation of {@link PerRobotCommentOperations}. Nevertheless, we keep
+ * the separation between interface and implementation to enhance clarity.
+ */
+public class PerRobotCommentOperationsImpl implements PerRobotCommentOperations {
+  private final CommentsUtil commentsUtil;
+
+  private final ChangeNotes changeNotes;
+  private final String commentUuid;
+
+  public interface Factory {
+    PerRobotCommentOperationsImpl create(ChangeNotes changeNotes, String commentUuid);
+  }
+
+  @Inject
+  public PerRobotCommentOperationsImpl(
+      CommentsUtil commentsUtil, @Assisted ChangeNotes changeNotes, @Assisted String commentUuid) {
+    this.commentsUtil = commentsUtil;
+    this.changeNotes = changeNotes;
+    this.commentUuid = commentUuid;
+  }
+
+  @Override
+  public TestRobotComment get() {
+    RobotComment comment =
+        commentsUtil.robotCommentsByChange(changeNotes).stream()
+            .filter(foundComment -> foundComment.key.uuid.equals(commentUuid))
+            .collect(onlyElement());
+    return toTestRobotComment(comment);
+  }
+
+  static TestRobotComment toTestRobotComment(RobotComment robotComment) {
+    return TestRobotComment.builder()
+        .uuid(robotComment.key.uuid)
+        .parentUuid(robotComment.parentUuid)
+        .build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java
new file mode 100644
index 0000000..b061c81
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import java.util.function.IntFunction;
+
+/**
+ * Builder to simplify a position specification. Used by {@link TestCommentCreation} and {@link
+ * TestRobotCommentCreation}.
+ */
+public class PositionBuilder<T> {
+  private final IntFunction<T> nextStepProvider;
+
+  public PositionBuilder(IntFunction<T> nextStepProvider) {
+    this.nextStepProvider = nextStepProvider;
+  }
+
+  /** Character offset within the line. A value of 0 indicates the beginning of the line. */
+  public T charOffset(int characterOffset) {
+    return nextStepProvider.apply(characterOffset);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java
new file mode 100644
index 0000000..b9639f5
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Builder for the end position of a range. Used by {@link TestCommentCreation} and {@link
+ * TestRobotCommentCreation}.
+ */
+public class StartAwarePositionBuilder<T> {
+  private final TestRange.Builder testRangeBuilder;
+  private final Consumer<TestRange> rangeConsumer;
+  private final Function<String, T> fileFunction;
+
+  public StartAwarePositionBuilder(
+      TestRange.Builder testRangeBuilder,
+      Consumer<TestRange> rangeConsumer,
+      Function<String, T> fileFunction) {
+    this.testRangeBuilder = testRangeBuilder;
+    this.rangeConsumer = rangeConsumer;
+    this.fileFunction = fileFunction;
+  }
+
+  /** Line of the end position of the range. */
+  public PositionBuilder<FileBuilder<T>> toLine(int endLine) {
+    return new PositionBuilder<>(
+        endCharOffset -> {
+          TestRange.Position end =
+              TestRange.Position.builder().line(endLine).charOffset(endCharOffset).build();
+          TestRange range = testRangeBuilder.setEnd(end).build();
+          rangeConsumer.accept(range);
+          return new FileBuilder<>(fileFunction);
+        });
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
new file mode 100644
index 0000000..ea2acaa
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Change;
+
+/** Representation of a change used for testing purposes. */
+@AutoValue
+public abstract class TestChange {
+
+  /**
+   * The numeric change ID, sometimes also called change number or legacy change ID. Unique per
+   * host.
+   */
+  public abstract Change.Id numericChangeId();
+
+  /**
+   * The Change-Id as specified in the commit message. Consists of an {@code I} followed by a 40-hex
+   * string. Only unique per project-branch.
+   */
+  public abstract String changeId();
+
+  static Builder builder() {
+    return new AutoValue_TestChange.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder numericChangeId(Change.Id numericChangeId);
+
+    abstract Builder changeId(String changeId);
+
+    abstract TestChange build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
new file mode 100644
index 0000000..5871e17
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.merge.MergeStrategy;
+
+/** Initial attributes of the change. If not provided, arbitrary values will be used. */
+@AutoValue
+public abstract class TestChangeCreation {
+  public abstract Optional<Project.NameKey> project();
+
+  public abstract String branch();
+
+  public abstract Optional<Account.Id> owner();
+
+  public abstract String commitMessage();
+
+  public abstract ImmutableList<TreeModification> treeModifications();
+
+  public abstract Optional<ImmutableList<TestCommitIdentifier>> parents();
+
+  public abstract MergeStrategy mergeStrategy();
+
+  abstract ThrowingFunction<TestChangeCreation, Change.Id> changeCreator();
+
+  public static Builder builder(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator) {
+    return new AutoValue_TestChangeCreation.Builder()
+        .changeCreator(changeCreator)
+        .branch(Constants.R_HEADS + Constants.MASTER)
+        .commitMessage("A test change")
+        // Which value we choose here doesn't matter. All relevant code paths set the desired value.
+        .mergeStrategy(MergeStrategy.OURS);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Target project/Repository of the change. Must be an existing project. */
+    public abstract Builder project(Project.NameKey project);
+
+    /**
+     * Target branch of the change. Neither needs to exist nor needs to point to an actual commit.
+     */
+    public abstract Builder branch(String branch);
+
+    /** The change owner. Must be an existing user account. */
+    public abstract Builder owner(Account.Id owner);
+
+    /**
+     * The commit message. The message may contain a {@code Change-Id} footer but does not need to.
+     * If the footer is absent, it will be generated.
+     */
+    public abstract Builder commitMessage(String commitMessage);
+
+    /** Modified file of the change. The file content is specified via the returned builder. */
+    public FileContentBuilder<Builder> file(String filePath) {
+      return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+    }
+
+    abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
+
+    /**
+     * Parent commit of the change. The commit can be specified via various means in the returned
+     * builder.
+     */
+    public ParentBuilder<Builder> childOf() {
+      return new ParentBuilder<>(parentCommit -> parents(ImmutableList.of(parentCommit)));
+    }
+
+    /**
+     * Parent commits of the change. Each parent commit can be specified via various means in the
+     * returned builder. The order of the parents matters and is preserved (first parent commit in
+     * fluent change -> first parent of the change).
+     *
+     * <p>This method will automatically merge the parent commits and use the resulting commit as
+     * base for the change. Use {@link #file(String)} for additional file adjustments on top of that
+     * merge commit.
+     *
+     * <p><strong>Note:</strong> If this method fails with a merge conflict, use {@link
+     * #mergeOfButBaseOnFirst()} instead and specify all other necessary file contents manually via
+     * {@link #file(String)}.
+     */
+    public ParentBuilder<MultipleParentBuilder<Builder>> mergeOf() {
+      return new ParentBuilder<>(parent -> mergeBuilder(MergeStrategy.RECURSIVE, parent));
+    }
+
+    /**
+     * Parent commits of the change. Each parent commit can be specified via various means in the
+     * returned builder. The order of the parents matters and is preserved (first parent commit in
+     * fluent change -> first parent of the change).
+     *
+     * <p>This method will use the first specified parent commit as base for the resulting change.
+     * This approach is especially useful if merging the parents is not possible.
+     */
+    public ParentBuilder<MultipleParentBuilder<Builder>> mergeOfButBaseOnFirst() {
+      return new ParentBuilder<>(parent -> mergeBuilder(MergeStrategy.OURS, parent));
+    }
+
+    MultipleParentBuilder<Builder> mergeBuilder(
+        MergeStrategy mergeStrategy, TestCommitIdentifier parent) {
+      mergeStrategy(mergeStrategy);
+      return new MultipleParentBuilder<>(this::parents, parent);
+    }
+
+    abstract Builder parents(ImmutableList<TestCommitIdentifier> parents);
+
+    abstract Builder mergeStrategy(MergeStrategy mergeStrategy);
+
+    abstract Builder changeCreator(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator);
+
+    abstract TestChangeCreation autoBuild();
+
+    /**
+     * Creates the change.
+     *
+     * @return the {@code Change.Id} of the created change
+     */
+    public Change.Id create() {
+      TestChangeCreation changeUpdate = autoBuild();
+      return changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
new file mode 100644
index 0000000..2031bde
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
@@ -0,0 +1,219 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Patch;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Optional;
+
+/**
+ * Attributes of the human comment. If not provided, arbitrary values will be used. This class is
+ * very similar to {@link TestRobotCommentCreation} to allow separation between robot and human
+ * comments.
+ */
+@AutoValue
+public abstract class TestCommentCreation {
+
+  public abstract Optional<String> message();
+
+  public abstract Optional<String> file();
+
+  public abstract Optional<Integer> line();
+
+  public abstract Optional<TestRange> range();
+
+  public abstract Optional<CommentSide> side();
+
+  public abstract Optional<Boolean> unresolved();
+
+  public abstract Optional<String> parentUuid();
+
+  public abstract Optional<String> tag();
+
+  public abstract Optional<Account.Id> author();
+
+  public abstract Optional<Instant> createdOn();
+
+  abstract Comment.Status status();
+
+  abstract ThrowingFunction<TestCommentCreation, String> commentCreator();
+
+  public static Builder builder(
+      ThrowingFunction<TestCommentCreation, String> commentCreator, Comment.Status commentStatus) {
+    return new AutoValue_TestCommentCreation.Builder()
+        .commentCreator(commentCreator)
+        .status(commentStatus);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public Builder noMessage() {
+      return message("");
+    }
+
+    /** Message text of the comment. */
+    public abstract Builder message(String message);
+
+    /** Indicates a patchset-level comment. */
+    public Builder onPatchsetLevel() {
+      return file(Patch.PATCHSET_LEVEL);
+    }
+
+    /** Indicates a file comment. The comment will be on the specified file. */
+    public Builder onFileLevelOf(String filePath) {
+      return file(filePath).line(null).range(null);
+    }
+
+    /**
+     * Starts the fluent change to create a line comment. The line comment will be at the indicated
+     * line. Lines start with 1.
+     */
+    public FileBuilder<Builder> onLine(int line) {
+      return new FileBuilder<>(file -> file(file).line(line).range(null));
+    }
+
+    /**
+     * Starts the fluent chain to create a range comment. The range begins at the specified line.
+     * Lines start at 1. The start position (line, charOffset) is inclusive, the end position (line,
+     * charOffset) is exclusive.
+     */
+    public PositionBuilder<StartAwarePositionBuilder<Builder>> fromLine(int startLine) {
+      return new PositionBuilder<>(
+          startCharOffset -> {
+            Position start = Position.builder().line(startLine).charOffset(startCharOffset).build();
+            TestRange.Builder testRangeBuilder = TestRange.builder().setStart(start);
+            return new StartAwarePositionBuilder<>(testRangeBuilder, this::range, this::file);
+          });
+    }
+
+    /** File on which the comment should be added. */
+    abstract Builder file(String filePath);
+
+    /** Line on which the comment should be added. */
+    abstract Builder line(@Nullable Integer line);
+
+    /** Range on which the comment should be added. */
+    abstract Builder range(@Nullable TestRange range);
+
+    /**
+     * Indicates that the comment refers to a file, line, range, ... in the commit of the patchset.
+     *
+     * <p>On the UI, such comments are shown on the right side of a diff view when a diff against
+     * base is selected. See {@link #onParentCommit()} for comments shown on the left side.
+     */
+    public Builder onPatchsetCommit() {
+      return side(CommentSide.PATCHSET_COMMIT);
+    }
+
+    /**
+     * Indicates that the comment refers to a file, line, range, ... in the parent commit of the
+     * patchset.
+     *
+     * <p>On the UI, such comments are shown on the left side of a diff view when a diff against
+     * base is selected. See {@link #onPatchsetCommit()} for comments shown on the right side.
+     *
+     * <p>For merge commits, this indicates the first parent commit.
+     */
+    public Builder onParentCommit() {
+      return side(CommentSide.PARENT_COMMIT);
+    }
+
+    /** Like {@link #onParentCommit()} but for the second parent of a merge commit. */
+    public Builder onSecondParentCommit() {
+      return side(CommentSide.SECOND_PARENT_COMMIT);
+    }
+
+    /**
+     * Like {@link #onParentCommit()} but for the AutoMerge commit created from the parents of a
+     * merge commit.
+     */
+    public Builder onAutoMergeCommit() {
+      return side(CommentSide.AUTO_MERGE_COMMIT);
+    }
+
+    abstract Builder side(CommentSide side);
+
+    /** Indicates a resolved comment. */
+    public Builder resolved() {
+      return unresolved(false);
+    }
+
+    /** Indicates an unresolved comment. */
+    public Builder unresolved() {
+      return unresolved(true);
+    }
+
+    abstract Builder unresolved(boolean unresolved);
+
+    /**
+     * UUID of another comment to which this comment is a reply. This comment must have similar
+     * attributes (e.g. file, line, side) as the parent comment. The parent comment must be a
+     * published comment.
+     */
+    public abstract Builder parentUuid(String parentUuid);
+
+    /** Tag to attach to the comment. */
+    public abstract Builder tag(String value);
+
+    /** Author of the comment. Must be an existing user account. */
+    public abstract Builder author(Account.Id accountId);
+
+    /**
+     * Creation time of the comment. Like {@link #createdOn(Instant)} but with an arbitrary, fixed
+     * time zone (-> deterministic test execution).
+     */
+    public Builder createdOn(LocalDateTime createdOn) {
+      // We don't care about the exact time zone in most tests, just that it's fixed so that tests
+      // are deterministic.
+      return createdOn(createdOn.atZone(ZoneOffset.UTC).toInstant());
+    }
+
+    /**
+     * Creation time of the comment. This may also lie in the past or future. Comments stored in
+     * NoteDb support only second precision.
+     */
+    public abstract Builder createdOn(Instant createdOn);
+
+    /**
+     * Status of the comment. Hidden in the API surface. Use {@link
+     * PerPatchsetOperations#newComment()} or {@link PerPatchsetOperations#newDraftComment()}
+     * depending on which type of comment you want to create.
+     */
+    abstract Builder status(Comment.Status value);
+
+    abstract Builder commentCreator(ThrowingFunction<TestCommentCreation, String> commentCreator);
+
+    abstract TestCommentCreation autoBuild();
+
+    /**
+     * Creates the comment.
+     *
+     * @return the UUID of the created comment
+     */
+    public String create() {
+      TestCommentCreation commentCreation = autoBuild();
+      return commentCreation.commentCreator().applyAndThrowSilently(commentCreation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java b/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java
new file mode 100644
index 0000000..a352607
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Attributes, each one uniquely identifying a commit. */
+@AutoOneOf(TestCommitIdentifier.Kind.class)
+public abstract class TestCommitIdentifier {
+  public enum Kind {
+    COMMIT_SHA_1,
+    BRANCH,
+    CHANGE_ID,
+    PATCHSET_ID
+  }
+
+  public abstract Kind getKind();
+
+  /** SHA-1 of the commit. */
+  public abstract ObjectId commitSha1();
+
+  /** Branch whose tip points to the desired commit. */
+  public abstract String branch();
+
+  /** Numeric ID of the change whose current patchset points to the desired commit. */
+  public abstract Change.Id changeId();
+
+  /** ID of the patchset representing the desired commit. */
+  public abstract PatchSet.Id patchsetId();
+
+  public static TestCommitIdentifier ofCommitSha1(ObjectId commitSha1) {
+    return AutoOneOf_TestCommitIdentifier.commitSha1(commitSha1);
+  }
+
+  public static TestCommitIdentifier ofBranch(String branchName) {
+    return AutoOneOf_TestCommitIdentifier.branch(branchName);
+  }
+
+  public static TestCommitIdentifier ofChangeId(Change.Id changeId) {
+    return AutoOneOf_TestCommitIdentifier.changeId(changeId);
+  }
+
+  public static TestCommitIdentifier ofPatchsetId(PatchSet.Id patchsetId) {
+    return AutoOneOf_TestCommitIdentifier.patchsetId(patchsetId);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java b/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java
new file mode 100644
index 0000000..3a7f2ae
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+/** Representation of a human comment used for testing purposes. */
+@AutoValue
+public abstract class TestHumanComment {
+
+  /** The UUID of the comment. Should be unique. */
+  public abstract String uuid();
+
+  /** UUID of another comment to which this comment is a reply. */
+  public abstract Optional<String> parentUuid();
+
+  /** Tag of a comment. */
+  public abstract Optional<String> tag();
+
+  /** Unresolved state of a comment. */
+  public abstract boolean unresolved();
+
+  static Builder builder() {
+    return new AutoValue_TestHumanComment.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder uuid(String uuid);
+
+    abstract Builder parentUuid(@Nullable String parentUuid);
+
+    abstract Builder tag(@Nullable String tag);
+
+    abstract Builder unresolved(boolean unresolved);
+
+    abstract TestHumanComment build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
new file mode 100644
index 0000000..1ba242a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.PatchSet;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Representation of a patchset used for testing purposes. */
+@AutoValue
+public abstract class TestPatchset {
+
+  /** The numeric patchset ID. */
+  public abstract PatchSet.Id patchsetId();
+
+  /** The commit SHA-1 of the patchset. */
+  public abstract ObjectId commitId();
+
+  static Builder builder() {
+    return new AutoValue_TestPatchset.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder patchsetId(PatchSet.Id patchsetId);
+
+    abstract Builder commitId(ObjectId commitId);
+
+    abstract TestPatchset build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
new file mode 100644
index 0000000..fe9d909
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import java.util.Optional;
+
+/** Initial attributes of the patchset. If not provided, arbitrary values will be used. */
+@AutoValue
+public abstract class TestPatchsetCreation {
+
+  public abstract Optional<String> commitMessage();
+
+  public abstract ImmutableList<TreeModification> treeModifications();
+
+  public abstract Optional<ImmutableList<TestCommitIdentifier>> parents();
+
+  abstract ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator();
+
+  public static TestPatchsetCreation.Builder builder(
+      ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator) {
+    return new AutoValue_TestPatchsetCreation.Builder().patchsetCreator(patchsetCreator);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder commitMessage(String commitMessage);
+
+    /** Modified file of the patchset. The file content is specified via the returned builder. */
+    public FileContentBuilder<Builder> file(String filePath) {
+      return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+    }
+
+    abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
+
+    /**
+     * Parent commit of the change. The commit can be specified via various means in the returned
+     * builder.
+     *
+     * <p>This method will just change the parent but not influence the contents of the patchset
+     * commit.
+     *
+     * <p>It's possible to switch from a change representing a merge commit to a change not being a
+     * merge commit with this method.
+     */
+    public ParentBuilder<Builder> parent() {
+      return new ParentBuilder<>(parent -> parents(ImmutableList.of(parent)));
+    }
+
+    /**
+     * Parent commits of the change. Each parent commit can be specified via various means in the
+     * returned builder. The order of the parents matters and is preserved (first parent commit in
+     * fluent change -> first parent of the change).
+     *
+     * <p>This method will just change the parents but not influence the contents of the patchset
+     * commit.
+     *
+     * <p>It's possible to switch from a change representing a non-merge commit to a change which is
+     * a merge commit with this method.
+     */
+    public ParentBuilder<MultipleParentBuilder<Builder>> parents() {
+      return new ParentBuilder<>(parent -> new MultipleParentBuilder<>(this::parents, parent));
+    }
+
+    abstract Builder parents(ImmutableList<TestCommitIdentifier> value);
+
+    abstract TestPatchsetCreation.Builder patchsetCreator(
+        ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator);
+
+    abstract TestPatchsetCreation autoBuild();
+
+    /**
+     * Creates the patchset.
+     *
+     * @return the {@code PatchSet.Id} of the created patchset
+     */
+    public PatchSet.Id create() {
+      TestPatchsetCreation patchsetCreation = autoBuild();
+      return patchsetCreation.patchsetCreator().applyAndThrowSilently(patchsetCreation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestRange.java b/java/com/google/gerrit/acceptance/testsuite/change/TestRange.java
new file mode 100644
index 0000000..f5cb7db
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestRange.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+
+/** Representation of a range used for testing purposes. */
+@AutoValue
+public abstract class TestRange {
+
+  /** Start position of the range. (inclusive) */
+  public abstract Position start();
+
+  /** End position of the range. (exclusive) */
+  public abstract Position end();
+
+  static Builder builder() {
+    return new AutoValue_TestRange.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+
+    abstract Builder setStart(Position start);
+
+    abstract Builder setEnd(Position end);
+
+    abstract TestRange build();
+  }
+
+  /** Position (start/end) of a range. */
+  @AutoValue
+  public abstract static class Position {
+
+    /** 1-based line. */
+    public abstract int line();
+
+    /** 0-based character offset within the line. */
+    public abstract int charOffset();
+
+    static Builder builder() {
+      return new AutoValue_TestRange_Position.Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+
+      abstract Builder line(int line);
+
+      abstract Builder charOffset(int characterOffset);
+
+      abstract Position build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java
new file mode 100644
index 0000000..76fb52f
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+/** Representation of a robot comment used for testing purposes. */
+@AutoValue
+public abstract class TestRobotComment {
+
+  /** The UUID of the comment. Should be unique. */
+  public abstract String uuid();
+
+  /** UUID of another comment to which this comment is a reply. */
+  public abstract Optional<String> parentUuid();
+
+  static Builder builder() {
+    return new AutoValue_TestRobotComment.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder uuid(String uuid);
+
+    abstract Builder parentUuid(@Nullable String parentUuid);
+
+    abstract TestRobotComment build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java
new file mode 100644
index 0000000..558af3f
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Patch;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Attributes of the robot comment. If not provided, arbitrary values will be used. This class is
+ * very similar to {@link TestCommentCreation} to allow separation between robot and human comments.
+ */
+@AutoValue
+public abstract class TestRobotCommentCreation {
+
+  public abstract Optional<String> message();
+
+  public abstract Optional<String> file();
+
+  public abstract Optional<Integer> line();
+
+  public abstract Optional<TestRange> range();
+
+  public abstract Optional<CommentSide> side();
+
+  public abstract Optional<String> parentUuid();
+
+  public abstract Optional<String> tag();
+
+  public abstract Optional<Account.Id> author();
+
+  public abstract Optional<String> robotId();
+
+  public abstract Optional<String> robotRunId();
+
+  public abstract Optional<String> url();
+
+  public abstract ImmutableMap<String, String> properties();
+
+  abstract ThrowingFunction<TestRobotCommentCreation, String> commentCreator();
+
+  public static Builder builder(ThrowingFunction<TestRobotCommentCreation, String> commentCreator) {
+    return new AutoValue_TestRobotCommentCreation.Builder().commentCreator(commentCreator);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public Builder noMessage() {
+      return message("");
+    }
+
+    /** Message text of the comment. */
+    public abstract Builder message(String message);
+
+    /** Indicates a patchset-level comment. */
+    public Builder onPatchsetLevel() {
+      return file(Patch.PATCHSET_LEVEL);
+    }
+
+    /** Indicates a file comment. The comment will be on the specified file. */
+    public Builder onFileLevelOf(String filePath) {
+      return file(filePath).line(null).range(null);
+    }
+
+    /**
+     * Starts the fluent change to create a line comment. The line comment will be at the indicated
+     * line. Lines start with 1.
+     */
+    public FileBuilder<Builder> onLine(int line) {
+      return new FileBuilder<>(file -> file(file).line(line).range(null));
+    }
+
+    /**
+     * Starts the fluent chain to create a range comment. The range begins at the specified line.
+     * Lines start at 1. The start position (line, charOffset) is inclusive, the end position (line,
+     * charOffset) is exclusive.
+     */
+    public PositionBuilder<StartAwarePositionBuilder<Builder>> fromLine(int startLine) {
+      return new PositionBuilder<>(
+          startCharOffset -> {
+            Position start = Position.builder().line(startLine).charOffset(startCharOffset).build();
+            TestRange.Builder testRangeBuilder = TestRange.builder().setStart(start);
+            return new StartAwarePositionBuilder<>(testRangeBuilder, this::range, this::file);
+          });
+    }
+
+    /** File on which the comment should be added. */
+    abstract Builder file(String filePath);
+
+    /** Line on which the comment should be added. */
+    abstract Builder line(@Nullable Integer line);
+
+    /** Range on which the comment should be added. */
+    abstract Builder range(@Nullable TestRange range);
+
+    /**
+     * Indicates that the comment refers to a file, line, range, ... in the commit of the patchset.
+     *
+     * <p>On the UI, such comments are shown on the right side of a diff view when a diff against
+     * base is selected. See {@link #onParentCommit()} for comments shown on the left side.
+     */
+    public Builder onPatchsetCommit() {
+      return side(CommentSide.PATCHSET_COMMIT);
+    }
+
+    /**
+     * Indicates that the comment refers to a file, line, range, ... in the parent commit of the
+     * patchset.
+     *
+     * <p>On the UI, such comments are shown on the left side of a diff view when a diff against
+     * base is selected. See {@link #onPatchsetCommit()} for comments shown on the right side.
+     *
+     * <p>For merge commits, this indicates the first parent commit.
+     */
+    public Builder onParentCommit() {
+      return side(CommentSide.PARENT_COMMIT);
+    }
+
+    /** Like {@link #onParentCommit()} but for the second parent of a merge commit. */
+    public Builder onSecondParentCommit() {
+      return side(CommentSide.SECOND_PARENT_COMMIT);
+    }
+
+    /**
+     * Like {@link #onParentCommit()} but for the AutoMerge commit created from the parents of a
+     * merge commit.
+     */
+    public Builder onAutoMergeCommit() {
+      return side(CommentSide.AUTO_MERGE_COMMIT);
+    }
+
+    abstract Builder side(CommentSide side);
+
+    /**
+     * UUID of another comment to which this comment is a reply. This comment must have similar
+     * attributes (e.g. file, line, side) as the parent comment. The parent comment must be a
+     * published comment.
+     */
+    public abstract Builder parentUuid(String parentUuid);
+
+    /** Tag to attach to the comment. */
+    public abstract Builder tag(String value);
+
+    /** Author of the comment. Must be an existing user account. */
+    public abstract Builder author(Account.Id accountId);
+
+    /** Id of the robot that created the comment. */
+    public abstract Builder robotId(String robotId);
+
+    /** An ID of the run of the robot that created the comment. */
+    public abstract Builder robotRunId(String robotRunId);
+
+    /** Url for more information for the robot comment. */
+    public abstract Builder url(String url);
+
+    /** Robot specific properties as map that maps arbitrary keys to values. */
+    public abstract Builder properties(Map<String, String> properties);
+
+    abstract ImmutableMap.Builder<String, String> propertiesBuilder();
+
+    public Builder addProperty(String key, String value) {
+      propertiesBuilder().put(key, value);
+      return this;
+    }
+
+    abstract Builder commentCreator(
+        ThrowingFunction<TestRobotCommentCreation, String> commentCreator);
+
+    abstract TestRobotCommentCreation autoBuild();
+
+    /**
+     * Creates the robot comment.
+     *
+     * @return the UUID of the created robot comment
+     */
+    public String create() {
+      TestRobotCommentCreation commentCreation = autoBuild();
+      return commentCreation.commentCreator().applyAndThrowSilently(commentCreation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 59a2ed3..394f0f8 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.testsuite.project;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -26,11 +27,11 @@
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.PermissionRule;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -43,12 +44,10 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import org.apache.commons.lang.RandomStringUtils;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -92,7 +91,8 @@
     CreateProjectArgs args = new CreateProjectArgs();
     args.setProjectName(name);
     args.permissionsOnly = projectCreation.permissionOnly().orElse(false);
-    args.branch = Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
+    args.branch =
+        projectCreation.branches().stream().map(RefNames::fullName).collect(toImmutableList());
     args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true);
     projectCreation.parent().ifPresent(p -> args.newParent = p);
     // ProjectCreator wants non-null owner IDs.
@@ -155,51 +155,52 @@
         ProjectConfig projectConfig,
         ImmutableList<TestProjectUpdate.TestPermissionKey> removedPermissions) {
       for (TestProjectUpdate.TestPermissionKey p : removedPermissions) {
-        Permission permission =
-            projectConfig.getAccessSection(p.section(), true).getPermission(p.name(), true);
-        if (p.group().isPresent()) {
-          GroupReference group = new GroupReference(p.group().get(), p.group().get().get());
-          group = projectConfig.resolve(group);
-          permission.removeRule(group);
-        } else {
-          permission.clearRules();
-        }
+        projectConfig.upsertAccessSection(
+            p.section(),
+            as -> {
+              Permission.Builder permission = as.upsertPermission(p.name());
+              if (p.group().isPresent()) {
+                GroupReference group =
+                    GroupReference.create(p.group().get(), p.group().get().get());
+                group = projectConfig.resolve(group);
+                permission.removeRule(group);
+              } else {
+                permission.clearRules();
+              }
+            });
       }
     }
 
     private void addCapabilities(
         ProjectConfig projectConfig, ImmutableList<TestCapability> addedCapabilities) {
       for (TestCapability c : addedCapabilities) {
-        PermissionRule rule = newRule(projectConfig, c.group());
+        PermissionRule.Builder rule = newRule(projectConfig, c.group());
         rule.setRange(c.min(), c.max());
-        projectConfig
-            .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-            .getPermission(c.name(), true)
-            .add(rule);
+        projectConfig.upsertAccessSection(
+            AccessSection.GLOBAL_CAPABILITIES, as -> as.upsertPermission(c.name()).add(rule));
       }
     }
 
     private void addPermissions(
         ProjectConfig projectConfig, ImmutableList<TestPermission> addedPermissions) {
       for (TestPermission p : addedPermissions) {
-        PermissionRule rule = newRule(projectConfig, p.group());
+        PermissionRule.Builder rule = newRule(projectConfig, p.group());
         rule.setAction(p.action());
         rule.setForce(p.force());
-        projectConfig.getAccessSection(p.ref(), true).getPermission(p.name(), true).add(rule);
+        projectConfig.upsertAccessSection(p.ref(), as -> as.upsertPermission(p.name()).add(rule));
       }
     }
 
     private void addLabelPermissions(
         ProjectConfig projectConfig, ImmutableList<TestLabelPermission> addedLabelPermissions) {
       for (TestLabelPermission p : addedLabelPermissions) {
-        PermissionRule rule = newRule(projectConfig, p.group());
+        PermissionRule.Builder rule = newRule(projectConfig, p.group());
         rule.setAction(p.action());
         rule.setRange(p.min(), p.max());
         String permissionName =
             p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
-        Permission permission =
-            projectConfig.getAccessSection(p.ref(), true).getPermission(permissionName, true);
-        permission.add(rule);
+        projectConfig.upsertAccessSection(
+            p.ref(), as -> as.upsertPermission(permissionName).add(rule));
       }
     }
 
@@ -208,16 +209,13 @@
         ImmutableMap<TestProjectUpdate.TestPermissionKey, Boolean> exclusiveGroupPermissions) {
       exclusiveGroupPermissions.forEach(
           (key, exclusive) ->
-              projectConfig
-                  .getAccessSection(key.section(), true)
-                  .getPermission(key.name(), true)
-                  .setExclusiveGroup(exclusive));
+              projectConfig.upsertAccessSection(
+                  key.section(),
+                  as -> as.upsertPermission(key.name()).setExclusiveGroup(exclusive)));
     }
 
     private RevCommit headOrNull(String branch) {
-      if (!branch.startsWith(Constants.R_REFS)) {
-        branch = RefNames.REFS_HEADS + branch;
-      }
+      branch = RefNames.fullName(branch);
 
       try (Repository repo = repoManager.openRepository(nameKey);
           RevWalk rw = new RevWalk(repo)) {
@@ -328,9 +326,10 @@
     }
   }
 
-  private static PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
-    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+  private static PermissionRule.Builder newRule(
+      ProjectConfig project, AccountGroup.UUID groupUUID) {
+    GroupReference group = GroupReference.create(groupUUID, groupUUID.get());
     group = project.resolve(group);
-    return new PermissionRule(group);
+    return PermissionRule.builder(group);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 3bbb8db..3337fc3 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -18,11 +18,14 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
 import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.Constants;
 
 @AutoValue
 public abstract class TestProjectCreation {
@@ -31,6 +34,8 @@
 
   public abstract Optional<Project.NameKey> parent();
 
+  public abstract ImmutableSet<String> branches();
+
   public abstract Optional<Boolean> createEmptyCommit();
 
   public abstract Optional<Boolean> permissionOnly();
@@ -43,7 +48,9 @@
 
   public static Builder builder(
       ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator) {
-    return new AutoValue_TestProjectCreation.Builder().projectCreator(projectCreator);
+    return new AutoValue_TestProjectCreation.Builder()
+        .branches(Constants.R_HEADS + Constants.MASTER)
+        .projectCreator(projectCreator);
   }
 
   @AutoValue.Builder
@@ -54,6 +61,17 @@
 
     public abstract TestProjectCreation.Builder submitType(SubmitType submitType);
 
+    /**
+     * Branches which should be created in the repository (with an empty root commit). The
+     * "refs/heads/" prefix of the branch name can be omitted. The specified branches are ignored if
+     * {@link #noEmptyCommit()} is used.
+     */
+    public TestProjectCreation.Builder branches(String branch1, String... otherBranches) {
+      return branches(Sets.union(ImmutableSet.of(branch1), ImmutableSet.copyOf(otherBranches)));
+    }
+
+    abstract TestProjectCreation.Builder branches(Set<String> branches);
+
     public abstract TestProjectCreation.Builder createEmptyCommit(boolean value);
 
     public abstract TestProjectCreation.Builder permissionOnly(boolean value);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index 739ed19..9a9a21a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.common.data.AccessSection.GLOBAL_CAPABILITIES;
+import static com.google.gerrit.entities.AccessSection.GLOBAL_CAPABILITIES;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -23,11 +23,11 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.PermissionRange;
+import com.google.gerrit.entities.PermissionRule;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/common/FooterConstants.java b/java/com/google/gerrit/common/FooterConstants.java
index 3ec809c..656d850 100644
--- a/java/com/google/gerrit/common/FooterConstants.java
+++ b/java/com/google/gerrit/common/FooterConstants.java
@@ -20,6 +20,9 @@
   /** The change ID as used to track patch sets. */
   public static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
+  /** Link is an alternative footer that may be used to track patch sets. */
+  public static final FooterKey LINK = new FooterKey("Link");
+
   /** The footer telling us who reviewed the change. */
   public static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
 
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 9f8b255..3e103c8 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -19,6 +19,8 @@
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
@@ -28,17 +30,29 @@
  */
 @Target({METHOD, TYPE, FIELD})
 @Retention(RUNTIME)
+@Repeatable(UsedAt.Uses.class)
 public @interface UsedAt {
   /** Enumeration of projects that call a method/type/field. */
   enum Project {
     GOOGLE,
     COLLABNET,
     PLUGIN_CHECKS,
+    PLUGIN_CODE_OWNERS,
     PLUGIN_DELETE_PROJECT,
     PLUGIN_SERVICEUSER,
+    PLUGIN_HIGH_AVAILABILITY,
+    PLUGIN_MULTI_SITE,
+    PLUGIN_WEBSESSION_FLATFILE,
     PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
   }
 
   /** Reference to the project that uses the method annotated with this annotation. */
   Project value();
+
+  /** Allows to mark method/type/field with multiple UsedAt annotations. */
+  @Retention(RUNTIME)
+  @Target(ElementType.TYPE)
+  @interface Uses {
+    UsedAt[] value();
+  }
 }
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
deleted file mode 100644
index 0c9663b..0000000
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ /dev/null
@@ -1,189 +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.common.data;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Project;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/** Portion of a {@link Project} describing access rules. */
-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/*";
-
-  /** 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 = "^";
-
-  /** 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() {
-    return permissions == null ? ImmutableList.of() : ImmutableList.copyOf(permissions);
-  }
-
-  public void setPermissions(List<Permission> list) {
-    requireNonNull(list);
-
-    Set<String> names = new HashSet<>();
-    for (Permission p : list) {
-      if (!names.add(p.getName().toLowerCase())) {
-        throw new IllegalArgumentException();
-      }
-    }
-
-    permissions = new ArrayList<>(list);
-  }
-
-  @Nullable
-  public Permission getPermission(String name) {
-    return getPermission(name, false);
-  }
-
-  @Nullable
-  public Permission getPermission(String name, boolean create) {
-    requireNonNull(name);
-
-    if (permissions != null) {
-      for (Permission p : permissions) {
-        if (p.getName().equalsIgnoreCase(name)) {
-          return p;
-        }
-      }
-    }
-
-    if (create) {
-      if (permissions == null) {
-        permissions = new ArrayList<>();
-      }
-
-      Permission p = new Permission(name);
-      permissions.add(p);
-      return p;
-    }
-
-    return null;
-  }
-
-  public void addPermission(Permission permission) {
-    requireNonNull(permission);
-
-    if (permissions == null) {
-      permissions = new ArrayList<>();
-    }
-
-    for (Permission p : permissions) {
-      if (p.getName().equalsIgnoreCase(permission.getName())) {
-        throw new IllegalArgumentException();
-      }
-    }
-
-    permissions.add(permission);
-  }
-
-  public void remove(Permission permission) {
-    requireNonNull(permission);
-    removePermission(permission.getName());
-  }
-
-  public void removePermission(String name) {
-    requireNonNull(name);
-
-    if (permissions != null) {
-      permissions.removeIf(permission -> name.equalsIgnoreCase(permission.getName()));
-    }
-  }
-
-  public void mergeFrom(AccessSection section) {
-    requireNonNull(section);
-
-    for (Permission src : section.getPermissions()) {
-      Permission dst = getPermission(src.getName());
-      if (dst != null) {
-        dst.mergeFrom(src);
-      } else {
-        permissions.add(src);
-      }
-    }
-  }
-
-  @Override
-  public int compareTo(AccessSection o) {
-    return comparePattern().compareTo(o.comparePattern());
-  }
-
-  private String comparePattern() {
-    if (getName().startsWith(REGEX_PREFIX)) {
-      return getName().substring(REGEX_PREFIX.length());
-    }
-    return getName();
-  }
-
-  @Override
-  public String toString() {
-    return "AccessSection[" + getName() + "]";
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof AccessSection)) {
-      return false;
-    }
-
-    AccessSection other = (AccessSection) obj;
-    if (!getName().equals(other.getName())) {
-      return false;
-    }
-    return new HashSet<>(getPermissions())
-        .equals(new HashSet<>(((AccessSection) obj).getPermissions()));
-  }
-
-  @Override
-  public int hashCode() {
-    int hashCode = super.hashCode();
-    if (permissions != null) {
-      for (Permission permission : permissions) {
-        hashCode += permission.hashCode();
-      }
-    }
-    hashCode += getName().hashCode();
-    return hashCode;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/CommentDetail.java b/java/com/google/gerrit/common/data/CommentDetail.java
index 55e0143..053764d 100644
--- a/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/java/com/google/gerrit/common/data/CommentDetail.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
 import java.util.ArrayList;
 import java.util.List;
@@ -36,7 +37,7 @@
 
   protected CommentDetail() {}
 
-  public void include(Change.Id changeId, Comment p) {
+  public void include(Change.Id changeId, HumanComment p) {
     PatchSet.Id psId = PatchSet.id(changeId, p.key.patchSetId);
     if (p.side == 0) {
       if (idA == null && idB.equals(psId)) {
diff --git a/java/com/google/gerrit/common/data/ContributorAgreement.java b/java/com/google/gerrit/common/data/ContributorAgreement.java
deleted file mode 100644
index bc106f0..0000000
--- a/java/com/google/gerrit/common/data/ContributorAgreement.java
+++ /dev/null
@@ -1,111 +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.data;
-
-import com.google.gerrit.entities.Project;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Portion of a {@link Project} describing a single contributor agreement. */
-public class ContributorAgreement implements Comparable<ContributorAgreement> {
-  protected String name;
-  protected String description;
-  protected List<PermissionRule> accepted;
-  protected GroupReference autoVerify;
-  protected String agreementUrl;
-  protected List<String> excludeProjectsRegexes;
-  protected List<String> matchProjectsRegexes;
-
-  protected ContributorAgreement() {}
-
-  public ContributorAgreement(String name) {
-    setName(name);
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String name) {
-    this.name = name;
-  }
-
-  public String getDescription() {
-    return description;
-  }
-
-  public void setDescription(String description) {
-    this.description = description;
-  }
-
-  public List<PermissionRule> getAccepted() {
-    if (accepted == null) {
-      accepted = new ArrayList<>();
-    }
-    return accepted;
-  }
-
-  public void setAccepted(List<PermissionRule> accepted) {
-    this.accepted = accepted;
-  }
-
-  public GroupReference getAutoVerify() {
-    return autoVerify;
-  }
-
-  public void setAutoVerify(GroupReference autoVerify) {
-    this.autoVerify = autoVerify;
-  }
-
-  public String getAgreementUrl() {
-    return agreementUrl;
-  }
-
-  public void setAgreementUrl(String agreementUrl) {
-    this.agreementUrl = agreementUrl;
-  }
-
-  public List<String> getExcludeProjectsRegexes() {
-    if (excludeProjectsRegexes == null) {
-      excludeProjectsRegexes = new ArrayList<>();
-    }
-    return excludeProjectsRegexes;
-  }
-
-  public void setExcludeProjectsRegexes(List<String> excludeProjectsRegexes) {
-    this.excludeProjectsRegexes = excludeProjectsRegexes;
-  }
-
-  public List<String> getMatchProjectsRegexes() {
-    if (matchProjectsRegexes == null) {
-      matchProjectsRegexes = new ArrayList<>();
-    }
-    return matchProjectsRegexes;
-  }
-
-  public void setMatchProjectsRegexes(List<String> matchProjectsRegexes) {
-    this.matchProjectsRegexes = matchProjectsRegexes;
-  }
-
-  @Override
-  public int compareTo(ContributorAgreement o) {
-    return getName().compareTo(o.getName());
-  }
-
-  @Override
-  public String toString() {
-    return "ContributorAgreement[" + getName() + "]";
-  }
-}
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 10a66cc..51d9ecd 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.PermissionRange;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
diff --git a/java/com/google/gerrit/common/data/GroupDescription.java b/java/com/google/gerrit/common/data/GroupDescription.java
deleted file mode 100644
index ed8b39d..0000000
--- a/java/com/google/gerrit/common/data/GroupDescription.java
+++ /dev/null
@@ -1,68 +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.data;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
-import java.util.Set;
-
-/** Group methods exposed by the GroupBackend. */
-public class GroupDescription {
-  /** The Basic information required to be exposed by any Group. */
-  public interface Basic {
-    /** @return the non-null UUID of the group. */
-    AccountGroup.UUID getGroupUUID();
-
-    /** @return the non-null name of the group. */
-    String getName();
-
-    /**
-     * @return optional email address to send to the group's members. If provided, Gerrit will use
-     *     this email address to send change notifications to the group.
-     */
-    @Nullable
-    String getEmailAddress();
-
-    /**
-     * @return optional URL to information about the group. Typically a URL to a web page that
-     *     permits users to apply to join the group, or manage their membership.
-     */
-    @Nullable
-    String getUrl();
-  }
-
-  /** The extended information exposed by internal groups. */
-  public interface Internal extends Basic {
-
-    AccountGroup.Id getId();
-
-    @Nullable
-    String getDescription();
-
-    AccountGroup.UUID getOwnerGroupUUID();
-
-    boolean isVisibleToAll();
-
-    Timestamp getCreatedOn();
-
-    Set<Account.Id> getMembers();
-
-    Set<AccountGroup.UUID> getSubgroups();
-  }
-
-  private GroupDescription() {}
-}
diff --git a/java/com/google/gerrit/common/data/GroupReference.java b/java/com/google/gerrit/common/data/GroupReference.java
deleted file mode 100644
index 0af088e..0000000
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ /dev/null
@@ -1,120 +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.common.data;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.AccountGroup;
-
-/** Describes a group within a projects {@link AccessSection}s. */
-public class GroupReference implements Comparable<GroupReference> {
-
-  private static final String PREFIX = "group ";
-
-  public static GroupReference forGroup(GroupDescription.Basic group) {
-    return new GroupReference(group.getGroupUUID(), group.getName());
-  }
-
-  public static boolean isGroupReference(String configValue) {
-    return configValue != null && configValue.startsWith(PREFIX);
-  }
-
-  @Nullable
-  public static String extractGroupName(String configValue) {
-    if (!isGroupReference(configValue)) {
-      return null;
-    }
-    return configValue.substring(PREFIX.length()).trim();
-  }
-
-  protected String uuid;
-  protected String name;
-
-  protected GroupReference() {}
-
-  /**
-   * Create a group reference.
-   *
-   * @param uuid UUID of the group, must not be {@code null}
-   * @param name the group name, must not be {@code null}
-   */
-  public GroupReference(AccountGroup.UUID uuid, String name) {
-    setUUID(requireNonNull(uuid));
-    setName(name);
-  }
-
-  /**
-   * Create a group reference where the group's name couldn't be resolved.
-   *
-   * @param name the group name, must not be {@code null}
-   */
-  public GroupReference(String name) {
-    setUUID(null);
-    setName(name);
-  }
-
-  @Nullable
-  public AccountGroup.UUID getUUID() {
-    return uuid != null ? AccountGroup.uuid(uuid) : null;
-  }
-
-  public void setUUID(@Nullable AccountGroup.UUID newUUID) {
-    uuid = newUUID != null ? newUUID.get() : null;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String newName) {
-    if (newName == null) {
-      throw new NullPointerException();
-    }
-    this.name = newName;
-  }
-
-  @Override
-  public int compareTo(GroupReference o) {
-    return uuid(this).compareTo(uuid(o));
-  }
-
-  private static String uuid(GroupReference a) {
-    if (a.getUUID() != null && a.getUUID().get() != null) {
-      return a.getUUID().get();
-    }
-
-    return "?";
-  }
-
-  @Override
-  public int hashCode() {
-    return uuid(this).hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return o instanceof GroupReference && compareTo((GroupReference) o) == 0;
-  }
-
-  public String toConfigValue() {
-    return PREFIX + name;
-  }
-
-  @Override
-  public String toString() {
-    return "Group[" + getName() + " / " + getUUID() + "]";
-  }
-}
diff --git a/java/com/google/gerrit/common/data/LabelFunction.java b/java/com/google/gerrit/common/data/LabelFunction.java
deleted file mode 100644
index 6af675b..0000000
--- a/java/com/google/gerrit/common/data/LabelFunction.java
+++ /dev/null
@@ -1,123 +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.common.data;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.PatchSetApproval;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Functions for determining submittability based on label votes.
- *
- * <p>Only describes built-in label functions. Admins can extend the logic arbitrarily using Prolog
- * rules, in which case the choice of function in the project config is ignored.
- *
- * <p>Function semantics are documented in {@code config-labels.txt}, and actual behavior is
- * implemented both in Prolog in {@code gerrit_common.pl} and in the {@link #check} method.
- */
-public enum LabelFunction {
-  ANY_WITH_BLOCK("AnyWithBlock", true, false, false),
-  MAX_WITH_BLOCK("MaxWithBlock", true, true, true),
-  MAX_NO_BLOCK("MaxNoBlock", false, true, true),
-  NO_BLOCK("NoBlock"),
-  NO_OP("NoOp"),
-  PATCH_SET_LOCK("PatchSetLock");
-
-  public static final Map<String, LabelFunction> ALL;
-
-  static {
-    Map<String, LabelFunction> all = new LinkedHashMap<>();
-    for (LabelFunction f : values()) {
-      all.put(f.getFunctionName(), f);
-    }
-    ALL = Collections.unmodifiableMap(all);
-  }
-
-  public static Optional<LabelFunction> parse(@Nullable String str) {
-    return Optional.ofNullable(ALL.get(str));
-  }
-
-  private final String name;
-  private final boolean isBlock;
-  private final boolean isRequired;
-  private final boolean requiresMaxValue;
-
-  LabelFunction(String name) {
-    this(name, false, false, false);
-  }
-
-  LabelFunction(String name, boolean isBlock, boolean isRequired, boolean requiresMaxValue) {
-    this.name = name;
-    this.isBlock = isBlock;
-    this.isRequired = isRequired;
-    this.requiresMaxValue = requiresMaxValue;
-  }
-
-  /** The function name as defined in documentation and {@code project.config}. */
-  public String getFunctionName() {
-    return name;
-  }
-
-  /** Whether the label is a "block" label, meaning a minimum vote will prevent submission. */
-  public boolean isBlock() {
-    return isBlock;
-  }
-
-  /** Whether the label is a mandatory label, meaning absence of votes will prevent submission. */
-  public boolean isRequired() {
-    return isRequired;
-  }
-
-  /** Whether the label requires a vote with the maximum value to allow submission. */
-  public boolean isMaxValueRequired() {
-    return requiresMaxValue;
-  }
-
-  public SubmitRecord.Label check(LabelType labelType, Iterable<PatchSetApproval> approvals) {
-    SubmitRecord.Label submitRecordLabel = new SubmitRecord.Label();
-    submitRecordLabel.label = labelType.getName();
-
-    submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
-    if (isRequired) {
-      submitRecordLabel.status = SubmitRecord.Label.Status.NEED;
-    }
-
-    for (PatchSetApproval a : approvals) {
-      if (a.value() == 0) {
-        continue;
-      }
-
-      if (isBlock && labelType.isMaxNegative(a)) {
-        submitRecordLabel.appliedBy = a.accountId();
-        submitRecordLabel.status = SubmitRecord.Label.Status.REJECT;
-        return submitRecordLabel;
-      }
-
-      if (labelType.isMaxPositive(a) || !requiresMaxValue) {
-        submitRecordLabel.appliedBy = a.accountId();
-
-        submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
-        if (isRequired) {
-          submitRecordLabel.status = SubmitRecord.Label.Status.OK;
-        }
-      }
-    }
-
-    return submitRecordLabel;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
deleted file mode 100644
index 3a68414..0000000
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ /dev/null
@@ -1,353 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.LabelId;
-import com.google.gerrit.entities.PatchSetApproval;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class LabelType {
-  public static final boolean DEF_ALLOW_POST_SUBMIT = true;
-  public static final boolean DEF_CAN_OVERRIDE = true;
-  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CHANGE = true;
-  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false;
-  public static final boolean DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = false;
-  public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
-  public static final boolean DEF_COPY_ANY_SCORE = false;
-  public static final boolean DEF_COPY_MAX_SCORE = false;
-  public static final boolean DEF_COPY_MIN_SCORE = false;
-  public static final ImmutableList<Short> DEF_COPY_VALUES = ImmutableList.of();
-  public static final boolean DEF_IGNORE_SELF_APPROVAL = false;
-
-  public static LabelType withDefaultValues(String name) {
-    checkName(name);
-    List<LabelValue> values = new ArrayList<>(2);
-    values.add(new LabelValue((short) 0, "Rejected"));
-    values.add(new LabelValue((short) 1, "Approved"));
-    return new LabelType(name, values);
-  }
-
-  public static String checkName(String name) {
-    checkNameInternal(name);
-    if ("SUBM".equals(name)) {
-      throw new IllegalArgumentException("Reserved label name \"" + name + "\"");
-    }
-    return name;
-  }
-
-  public static String checkNameInternal(String name) {
-    if (name == null || name.isEmpty()) {
-      throw new IllegalArgumentException("Empty label name");
-    }
-    for (int i = 0; i < name.length(); i++) {
-      char c = name.charAt(i);
-      if ((i == 0 && c == '-')
-          || !((c >= 'a' && c <= 'z')
-              || (c >= 'A' && c <= 'Z')
-              || (c >= '0' && c <= '9')
-              || c == '-')) {
-        throw new IllegalArgumentException("Illegal label name \"" + name + "\"");
-      }
-    }
-    return name;
-  }
-
-  private static List<LabelValue> sortValues(List<LabelValue> values) {
-    values = new ArrayList<>(values);
-    if (values.isEmpty()) {
-      return Collections.emptyList();
-    }
-    values = values.stream().sorted(comparing(LabelValue::getValue)).collect(toList());
-    short v = values.get(0).getValue();
-    short i = 0;
-    ArrayList<LabelValue> result = new ArrayList<>();
-    // Fill in any missing values with empty text.
-    while (i < values.size()) {
-      while (v < values.get(i).getValue()) {
-        result.add(new LabelValue(v++, ""));
-      }
-      v++;
-      result.add(values.get(i++));
-    }
-    result.trimToSize();
-    return Collections.unmodifiableList(result);
-  }
-
-  protected String name;
-
-  protected LabelFunction function;
-
-  protected boolean copyAnyScore;
-  protected boolean copyMinScore;
-  protected boolean copyMaxScore;
-  protected boolean copyAllScoresOnMergeFirstParentUpdate;
-  protected boolean copyAllScoresOnTrivialRebase;
-  protected boolean copyAllScoresIfNoCodeChange;
-  protected boolean copyAllScoresIfNoChange;
-  protected ImmutableList<Short> copyValues;
-  protected boolean allowPostSubmit;
-  protected boolean ignoreSelfApproval;
-  protected short defaultValue;
-
-  protected List<LabelValue> values;
-  protected short maxNegative;
-  protected short maxPositive;
-
-  private transient boolean canOverride;
-  private transient List<String> refPatterns;
-  private transient Map<Short, LabelValue> byValue;
-
-  protected LabelType() {}
-
-  public LabelType(String name, List<LabelValue> valueList) {
-    this.name = checkName(name);
-    canOverride = true;
-    values = sortValues(valueList);
-    defaultValue = 0;
-
-    function = LabelFunction.MAX_WITH_BLOCK;
-
-    maxNegative = Short.MIN_VALUE;
-    maxPositive = Short.MAX_VALUE;
-    if (!values.isEmpty()) {
-      if (values.get(0).getValue() < 0) {
-        maxNegative = values.get(0).getValue();
-      }
-      if (values.get(values.size() - 1).getValue() > 0) {
-        maxPositive = values.get(values.size() - 1).getValue();
-      }
-    }
-    setCanOverride(DEF_CAN_OVERRIDE);
-    setCopyAllScoresIfNoChange(DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
-    setCopyAllScoresIfNoCodeChange(DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
-    setCopyAllScoresOnTrivialRebase(DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
-    setCopyAllScoresOnMergeFirstParentUpdate(DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
-    setCopyAnyScore(DEF_COPY_ANY_SCORE);
-    setCopyMaxScore(DEF_COPY_MAX_SCORE);
-    setCopyMinScore(DEF_COPY_MIN_SCORE);
-    setCopyValues(DEF_COPY_VALUES);
-    setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT);
-    setIgnoreSelfApproval(DEF_IGNORE_SELF_APPROVAL);
-
-    byValue = new HashMap<>();
-    for (LabelValue v : values) {
-      byValue.put(v.getValue(), v);
-    }
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String name) {
-    this.name = checkName(name);
-  }
-
-  public boolean matches(PatchSetApproval psa) {
-    return psa.labelId().get().equalsIgnoreCase(name);
-  }
-
-  public LabelFunction getFunction() {
-    return function;
-  }
-
-  public void setFunction(@Nullable LabelFunction function) {
-    this.function = function;
-  }
-
-  public boolean canOverride() {
-    return canOverride;
-  }
-
-  @Nullable
-  public List<String> getRefPatterns() {
-    return refPatterns;
-  }
-
-  public void setCanOverride(boolean canOverride) {
-    this.canOverride = canOverride;
-  }
-
-  public boolean allowPostSubmit() {
-    return allowPostSubmit;
-  }
-
-  public void setAllowPostSubmit(boolean allowPostSubmit) {
-    this.allowPostSubmit = allowPostSubmit;
-  }
-
-  public boolean ignoreSelfApproval() {
-    return ignoreSelfApproval;
-  }
-
-  public void setIgnoreSelfApproval(boolean ignoreSelfApproval) {
-    this.ignoreSelfApproval = ignoreSelfApproval;
-  }
-
-  public void setRefPatterns(List<String> refPatterns) {
-    if (refPatterns != null && !refPatterns.isEmpty()) {
-      this.refPatterns =
-          refPatterns.stream().collect(collectingAndThen(toList(), Collections::unmodifiableList));
-    } else {
-      this.refPatterns = null;
-    }
-  }
-
-  public List<LabelValue> getValues() {
-    return values;
-  }
-
-  public void setValues(List<LabelValue> values) {
-    this.values = sortValues(values);
-  }
-
-  public LabelValue getMin() {
-    if (values.isEmpty()) {
-      return null;
-    }
-    return values.get(0);
-  }
-
-  public LabelValue getMax() {
-    if (values.isEmpty()) {
-      return null;
-    }
-    return values.get(values.size() - 1);
-  }
-
-  public short getDefaultValue() {
-    return defaultValue;
-  }
-
-  public void setDefaultValue(short defaultValue) {
-    this.defaultValue = defaultValue;
-  }
-
-  public boolean isCopyAnyScore() {
-    return copyAnyScore;
-  }
-
-  public void setCopyAnyScore(boolean copyAnyScore) {
-    this.copyAnyScore = copyAnyScore;
-  }
-
-  public boolean isCopyMinScore() {
-    return copyMinScore;
-  }
-
-  public void setCopyMinScore(boolean copyMinScore) {
-    this.copyMinScore = copyMinScore;
-  }
-
-  public boolean isCopyMaxScore() {
-    return copyMaxScore;
-  }
-
-  public void setCopyMaxScore(boolean copyMaxScore) {
-    this.copyMaxScore = copyMaxScore;
-  }
-
-  public boolean isCopyAllScoresOnMergeFirstParentUpdate() {
-    return copyAllScoresOnMergeFirstParentUpdate;
-  }
-
-  public void setCopyAllScoresOnMergeFirstParentUpdate(
-      boolean copyAllScoresOnMergeFirstParentUpdate) {
-    this.copyAllScoresOnMergeFirstParentUpdate = copyAllScoresOnMergeFirstParentUpdate;
-  }
-
-  public boolean isCopyAllScoresOnTrivialRebase() {
-    return copyAllScoresOnTrivialRebase;
-  }
-
-  public void setCopyAllScoresOnTrivialRebase(boolean copyAllScoresOnTrivialRebase) {
-    this.copyAllScoresOnTrivialRebase = copyAllScoresOnTrivialRebase;
-  }
-
-  public boolean isCopyAllScoresIfNoCodeChange() {
-    return copyAllScoresIfNoCodeChange;
-  }
-
-  public void setCopyAllScoresIfNoCodeChange(boolean copyAllScoresIfNoCodeChange) {
-    this.copyAllScoresIfNoCodeChange = copyAllScoresIfNoCodeChange;
-  }
-
-  public boolean isCopyAllScoresIfNoChange() {
-    return copyAllScoresIfNoChange;
-  }
-
-  public void setCopyAllScoresIfNoChange(boolean copyAllScoresIfNoChange) {
-    this.copyAllScoresIfNoChange = copyAllScoresIfNoChange;
-  }
-
-  public ImmutableList<Short> getCopyValues() {
-    return copyValues;
-  }
-
-  public void setCopyValues(Collection<Short> copyValues) {
-    this.copyValues = copyValues.stream().sorted().collect(toImmutableList());
-  }
-
-  public boolean isMaxNegative(PatchSetApproval ca) {
-    return maxNegative == ca.value();
-  }
-
-  public boolean isMaxPositive(PatchSetApproval ca) {
-    return maxPositive == ca.value();
-  }
-
-  public LabelValue getValue(short value) {
-    return byValue.get(value);
-  }
-
-  public LabelValue getValue(PatchSetApproval ca) {
-    return byValue.get(ca.value());
-  }
-
-  public LabelId getLabelId() {
-    return LabelId.create(name);
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder(name).append('[');
-    LabelValue min = getMin();
-    LabelValue max = getMax();
-    if (min != null && max != null) {
-      sb.append(
-          new PermissionRange(Permission.forLabel(name), min.getValue(), max.getValue())
-              .toString()
-              .trim());
-    } else if (min != null) {
-      sb.append(min.formatValue().trim());
-    } else if (max != null) {
-      sb.append(max.formatValue().trim());
-    }
-    sb.append(']');
-    return sb.toString();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/LabelTypes.java b/java/com/google/gerrit/common/data/LabelTypes.java
deleted file mode 100644
index 1647658..0000000
--- a/java/com/google/gerrit/common/data/LabelTypes.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.entities.LabelId;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class LabelTypes {
-  protected List<LabelType> labelTypes;
-  private transient volatile Map<String, LabelType> byLabel;
-  private transient volatile Map<String, Integer> positions;
-
-  protected LabelTypes() {}
-
-  public LabelTypes(List<? extends LabelType> approvals) {
-    labelTypes = Collections.unmodifiableList(new ArrayList<>(approvals));
-  }
-
-  public List<LabelType> getLabelTypes() {
-    return labelTypes;
-  }
-
-  public LabelType byLabel(LabelId labelId) {
-    return byLabel().get(labelId.get().toLowerCase());
-  }
-
-  public LabelType byLabel(String labelName) {
-    return byLabel().get(labelName.toLowerCase());
-  }
-
-  private Map<String, LabelType> byLabel() {
-    if (byLabel == null) {
-      synchronized (this) {
-        if (byLabel == null) {
-          Map<String, LabelType> l = new HashMap<>();
-          if (labelTypes != null) {
-            for (LabelType t : labelTypes) {
-              l.put(t.getName().toLowerCase(), t);
-            }
-          }
-          byLabel = l;
-        }
-      }
-    }
-    return byLabel;
-  }
-
-  @Override
-  public String toString() {
-    return labelTypes.toString();
-  }
-
-  public Comparator<String> nameComparator() {
-    final Map<String, Integer> positions = positions();
-    return new Comparator<String>() {
-      @Override
-      public int compare(String left, String right) {
-        int lp = position(left);
-        int rp = position(right);
-        int cmp = lp - rp;
-        if (cmp == 0) {
-          cmp = left.compareTo(right);
-        }
-        return cmp;
-      }
-
-      private int position(String name) {
-        Integer p = positions.get(name);
-        return p != null ? p : positions.size();
-      }
-    };
-  }
-
-  private Map<String, Integer> positions() {
-    if (positions == null) {
-      synchronized (this) {
-        if (positions == null) {
-          Map<String, Integer> p = new HashMap<>();
-          if (labelTypes != null) {
-            int i = 0;
-            for (LabelType t : labelTypes) {
-              p.put(t.getName(), i++);
-            }
-          }
-          positions = p;
-        }
-      }
-    }
-    return positions;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/LabelValue.java b/java/com/google/gerrit/common/data/LabelValue.java
deleted file mode 100644
index c0ba781..0000000
--- a/java/com/google/gerrit/common/data/LabelValue.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import java.util.Objects;
-
-public class LabelValue {
-  public static String formatValue(short value) {
-    if (value < 0) {
-      return Short.toString(value);
-    } else if (value == 0) {
-      return " 0";
-    } else {
-      return "+" + Short.toString(value);
-    }
-  }
-
-  protected short value;
-  protected String text;
-
-  public LabelValue(short value, String text) {
-    this.value = value;
-    this.text = text;
-  }
-
-  protected LabelValue() {}
-
-  public short getValue() {
-    return value;
-  }
-
-  public String getText() {
-    return text;
-  }
-
-  public String formatValue() {
-    return formatValue(value);
-  }
-
-  public String format() {
-    StringBuilder sb = new StringBuilder(formatValue());
-    if (!text.isEmpty()) {
-      sb.append(' ').append(text);
-    }
-    return sb.toString();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof LabelValue)) {
-      return false;
-    }
-    LabelValue v = (LabelValue) o;
-    return value == v.value && Objects.equals(text, v.text);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(value, text);
-  }
-
-  @Override
-  public String toString() {
-    return format();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
deleted file mode 100644
index 9b86b7e..0000000
--- a/java/com/google/gerrit/common/data/Permission.java
+++ /dev/null
@@ -1,302 +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.common.data;
-
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-
-/** A single permission within an {@link AccessSection} of a project. */
-public class Permission implements Comparable<Permission> {
-  public static final String ABANDON = "abandon";
-  public static final String ADD_PATCH_SET = "addPatchSet";
-  public static final String CREATE = "create";
-  public static final String CREATE_SIGNED_TAG = "createSignedTag";
-  public static final String CREATE_TAG = "createTag";
-  public static final String DELETE = "delete";
-  public static final String DELETE_CHANGES = "deleteChanges";
-  public static final String DELETE_OWN_CHANGES = "deleteOwnChanges";
-  public static final String EDIT_ASSIGNEE = "editAssignee";
-  public static final String EDIT_HASHTAGS = "editHashtags";
-  public static final String EDIT_TOPIC_NAME = "editTopicName";
-  public static final String FORGE_AUTHOR = "forgeAuthor";
-  public static final String FORGE_COMMITTER = "forgeCommitter";
-  public static final String FORGE_SERVER = "forgeServerAsCommitter";
-  public static final String LABEL = "label-";
-  public static final String LABEL_AS = "labelAs-";
-  public static final String OWNER = "owner";
-  public static final String PUSH = "push";
-  public static final String PUSH_MERGE = "pushMerge";
-  public static final String READ = "read";
-  public static final String REBASE = "rebase";
-  public static final String REMOVE_REVIEWER = "removeReviewer";
-  public static final String REVERT = "revert";
-  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;
-  private static final int LABEL_INDEX;
-  private static final int LABEL_AS_INDEX;
-
-  static {
-    NAMES_LC = new ArrayList<>();
-    NAMES_LC.add(ABANDON.toLowerCase());
-    NAMES_LC.add(ADD_PATCH_SET.toLowerCase());
-    NAMES_LC.add(CREATE.toLowerCase());
-    NAMES_LC.add(CREATE_SIGNED_TAG.toLowerCase());
-    NAMES_LC.add(CREATE_TAG.toLowerCase());
-    NAMES_LC.add(DELETE.toLowerCase());
-    NAMES_LC.add(DELETE_CHANGES.toLowerCase());
-    NAMES_LC.add(DELETE_OWN_CHANGES.toLowerCase());
-    NAMES_LC.add(EDIT_ASSIGNEE.toLowerCase());
-    NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
-    NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
-    NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
-    NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
-    NAMES_LC.add(FORGE_SERVER.toLowerCase());
-    NAMES_LC.add(LABEL.toLowerCase());
-    NAMES_LC.add(LABEL_AS.toLowerCase());
-    NAMES_LC.add(OWNER.toLowerCase());
-    NAMES_LC.add(PUSH.toLowerCase());
-    NAMES_LC.add(PUSH_MERGE.toLowerCase());
-    NAMES_LC.add(READ.toLowerCase());
-    NAMES_LC.add(REBASE.toLowerCase());
-    NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
-    NAMES_LC.add(REVERT.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);
-    LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
-  }
-
-  /** @return true if the name is recognized as a permission name. */
-  public static boolean isPermission(String varName) {
-    return isLabel(varName) || isLabelAs(varName) || NAMES_LC.contains(varName.toLowerCase());
-  }
-
-  public static boolean hasRange(String varName) {
-    return isLabel(varName) || isLabelAs(varName);
-  }
-
-  /** @return true if the permission name is actually for a review label. */
-  public static boolean isLabel(String varName) {
-    return varName.startsWith(LABEL) && LABEL.length() < varName.length();
-  }
-
-  /** @return true if the permission is for impersonated review labels. */
-  public static boolean isLabelAs(String var) {
-    return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
-  }
-
-  /** @return permission name for the given review label. */
-  public static String forLabel(String labelName) {
-    return LABEL + labelName;
-  }
-
-  /** @return permission name to apply a label for another user. */
-  public static String forLabelAs(String labelName) {
-    return LABEL_AS + labelName;
-  }
-
-  public static String extractLabel(String varName) {
-    if (isLabel(varName)) {
-      return varName.substring(LABEL.length());
-    } else if (isLabelAs(varName)) {
-      return varName.substring(LABEL_AS.length());
-    }
-    return null;
-  }
-
-  public static boolean canBeOnAllProjects(String ref, String permissionName) {
-    if (AccessSection.ALL.equals(ref)) {
-      return !OWNER.equals(permissionName);
-    }
-    return true;
-  }
-
-  protected String name;
-  protected boolean exclusiveGroup;
-  protected List<PermissionRule> rules;
-
-  protected Permission() {}
-
-  public Permission(String name) {
-    this.name = name;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public String getLabel() {
-    return extractLabel(getName());
-  }
-
-  public boolean getExclusiveGroup() {
-    // Only permit exclusive group behavior on non OWNER permissions,
-    // otherwise an owner might lose access to a delegated subspace.
-    //
-    return exclusiveGroup && !OWNER.equals(getName());
-  }
-
-  public void setExclusiveGroup(boolean newExclusiveGroup) {
-    exclusiveGroup = newExclusiveGroup;
-  }
-
-  public ImmutableList<PermissionRule> getRules() {
-    return rules == null ? ImmutableList.of() : ImmutableList.copyOf(rules);
-  }
-
-  public void setRules(List<PermissionRule> list) {
-    rules = new ArrayList<>(list);
-  }
-
-  public void add(PermissionRule rule) {
-    initRules();
-    rules.add(rule);
-  }
-
-  public void remove(PermissionRule rule) {
-    if (rule != null) {
-      removeRule(rule.getGroup());
-    }
-  }
-
-  public void removeRule(GroupReference group) {
-    if (rules != null) {
-      rules.removeIf(permissionRule -> sameGroup(permissionRule, group));
-    }
-  }
-
-  public void clearRules() {
-    if (rules != null) {
-      rules.clear();
-    }
-  }
-
-  public PermissionRule getRule(GroupReference group) {
-    return getRule(group, false);
-  }
-
-  public PermissionRule getRule(GroupReference group, boolean create) {
-    initRules();
-
-    for (PermissionRule r : rules) {
-      if (sameGroup(r, group)) {
-        return r;
-      }
-    }
-
-    if (create) {
-      PermissionRule r = new PermissionRule(group);
-      rules.add(r);
-      return r;
-    }
-    return null;
-  }
-
-  void mergeFrom(Permission src) {
-    for (PermissionRule srcRule : src.getRules()) {
-      PermissionRule dstRule = getRule(srcRule.getGroup());
-      if (dstRule != null) {
-        dstRule.mergeFrom(srcRule);
-      } else {
-        add(srcRule);
-      }
-    }
-  }
-
-  private static boolean sameGroup(PermissionRule rule, GroupReference group) {
-    if (group.getUUID() != null) {
-      return group.getUUID().equals(rule.getGroup().getUUID());
-
-    } else if (group.getName() != null) {
-      return group.getName().equals(rule.getGroup().getName());
-
-    } else {
-      return false;
-    }
-  }
-
-  private void initRules() {
-    if (rules == null) {
-      rules = new ArrayList<>(4);
-    }
-  }
-
-  @Override
-  public int compareTo(Permission b) {
-    int cmp = index(this) - index(b);
-    if (cmp == 0) {
-      cmp = getName().compareTo(b.getName());
-    }
-    return cmp;
-  }
-
-  private static int index(Permission a) {
-    if (isLabel(a.getName())) {
-      return LABEL_INDEX;
-    } else if (isLabelAs(a.getName())) {
-      return LABEL_AS_INDEX;
-    }
-
-    int index = NAMES_LC.indexOf(a.getName().toLowerCase());
-    return 0 <= index ? index : NAMES_LC.size();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof Permission)) {
-      return false;
-    }
-
-    final Permission other = (Permission) obj;
-    if (!name.equals(other.name) || exclusiveGroup != other.exclusiveGroup) {
-      return false;
-    }
-    return new HashSet<>(getRules()).equals(new HashSet<>(other.getRules()));
-  }
-
-  @Override
-  public int hashCode() {
-    return name.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder bldr = new StringBuilder();
-    bldr.append(name).append(" ");
-    if (exclusiveGroup) {
-      bldr.append("[exclusive] ");
-    }
-    bldr.append("[");
-    Iterator<PermissionRule> it = getRules().iterator();
-    while (it.hasNext()) {
-      bldr.append(it.next());
-      if (it.hasNext()) {
-        bldr.append(", ");
-      }
-    }
-    bldr.append("]");
-    return bldr.toString();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/PermissionRange.java b/java/com/google/gerrit/common/data/PermissionRange.java
deleted file mode 100644
index 97c3731..0000000
--- a/java/com/google/gerrit/common/data/PermissionRange.java
+++ /dev/null
@@ -1,144 +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.common.data;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents a closed interval [min, max] with a name. The special value [0, 0] is understood to be
- * the empty range.
- */
-public class PermissionRange implements Comparable<PermissionRange> {
-  public static class WithDefaults extends PermissionRange {
-    protected int defaultMin;
-    protected int defaultMax;
-
-    protected WithDefaults() {}
-
-    public WithDefaults(String name, int min, int max, int defMin, int defMax) {
-      super(name, min, max);
-      setDefaultRange(defMin, defMax);
-    }
-
-    public int getDefaultMin() {
-      return defaultMin;
-    }
-
-    public int getDefaultMax() {
-      return defaultMax;
-    }
-
-    public void setDefaultRange(int min, int max) {
-      defaultMin = min;
-      defaultMax = max;
-    }
-
-    /** @return all values between {@link #getMin()} and {@link #getMax()} */
-    public List<Integer> getValuesAsList() {
-      ArrayList<Integer> r = new ArrayList<>(getRangeSize());
-      for (int i = min; i <= max; i++) {
-        r.add(i);
-      }
-      return r;
-    }
-
-    /** @return number of values between {@link #getMin()} and {@link #getMax()} */
-    public int getRangeSize() {
-      return max - min;
-    }
-  }
-
-  protected String name;
-  protected int min;
-  protected int max;
-
-  protected PermissionRange() {}
-
-  public PermissionRange(String name, int min, int max) {
-    this.name = name;
-
-    if (min <= max) {
-      this.min = min;
-      this.max = max;
-    } else {
-      this.min = 0;
-      this.max = 0;
-    }
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public boolean isLabel() {
-    return Permission.isLabel(getName());
-  }
-
-  public String getLabel() {
-    return Permission.extractLabel(getName());
-  }
-
-  public int getMin() {
-    return min;
-  }
-
-  public int getMax() {
-    return max;
-  }
-
-  /** True if the value is within the range. */
-  public boolean contains(int value) {
-    return getMin() <= value && value <= getMax();
-  }
-
-  /** Normalize the value to fit within the bounds of the range. */
-  public int squash(int value) {
-    return Math.min(Math.max(getMin(), value), getMax());
-  }
-
-  /** True both {@link #getMin()} and {@link #getMax()} are 0. */
-  public boolean isEmpty() {
-    return getMin() == 0 && getMax() == 0;
-  }
-
-  @Override
-  public int compareTo(PermissionRange o) {
-    return getName().compareTo(o.getName());
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder r = new StringBuilder();
-    if (getMin() < 0 && getMax() == 0) {
-      r.append(getMin());
-      r.append(' ');
-    } else {
-      if (getMin() != getMax()) {
-        if (0 <= getMin()) {
-          r.append('+');
-        }
-        r.append(getMin());
-        r.append("..");
-      }
-      if (0 <= getMax()) {
-        r.append('+');
-      }
-      r.append(getMax());
-      r.append(' ');
-    }
-    return r.toString();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/PermissionRule.java b/java/com/google/gerrit/common/data/PermissionRule.java
deleted file mode 100644
index 8ab0a55..0000000
--- a/java/com/google/gerrit/common/data/PermissionRule.java
+++ /dev/null
@@ -1,296 +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.common.data;
-
-public class PermissionRule implements Comparable<PermissionRule> {
-  public static final String FORCE_PUSH = "Force Push";
-  public static final String FORCE_EDIT = "Force Edit";
-
-  public enum Action {
-    ALLOW,
-    DENY,
-    BLOCK,
-
-    INTERACTIVE,
-    BATCH
-  }
-
-  protected Action action = Action.ALLOW;
-  protected boolean force;
-  protected int min;
-  protected int max;
-  protected GroupReference group;
-
-  public PermissionRule() {}
-
-  public PermissionRule(GroupReference group) {
-    this.group = group;
-  }
-
-  public Action getAction() {
-    return action;
-  }
-
-  public void setAction(Action action) {
-    if (action == null) {
-      throw new NullPointerException("action");
-    }
-    this.action = action;
-  }
-
-  public boolean isDeny() {
-    return action == Action.DENY;
-  }
-
-  public void setDeny() {
-    action = Action.DENY;
-  }
-
-  public boolean isBlock() {
-    return action == Action.BLOCK;
-  }
-
-  public void setBlock() {
-    action = Action.BLOCK;
-  }
-
-  public boolean getForce() {
-    return force;
-  }
-
-  public void setForce(boolean newForce) {
-    force = newForce;
-  }
-
-  public int getMin() {
-    return min;
-  }
-
-  public void setMin(int min) {
-    this.min = min;
-  }
-
-  public void setMax(int max) {
-    this.max = max;
-  }
-
-  public int getMax() {
-    return max;
-  }
-
-  public void setRange(int newMin, int newMax) {
-    if (newMax < newMin) {
-      min = newMax;
-      max = newMin;
-    } else {
-      min = newMin;
-      max = newMax;
-    }
-  }
-
-  public GroupReference getGroup() {
-    return group;
-  }
-
-  public void setGroup(GroupReference newGroup) {
-    group = newGroup;
-  }
-
-  void mergeFrom(PermissionRule src) {
-    if (getAction() != src.getAction()) {
-      if (getAction() == Action.BLOCK || src.getAction() == Action.BLOCK) {
-        setAction(Action.BLOCK);
-
-      } else if (getAction() == Action.DENY || src.getAction() == Action.DENY) {
-        setAction(Action.DENY);
-
-      } else if (getAction() == Action.BATCH || src.getAction() == Action.BATCH) {
-        setAction(Action.BATCH);
-      }
-    }
-
-    setForce(getForce() || src.getForce());
-    setRange(Math.min(getMin(), src.getMin()), Math.max(getMax(), src.getMax()));
-  }
-
-  @Override
-  public int compareTo(PermissionRule o) {
-    int cmp = action(this) - action(o);
-    if (cmp == 0) {
-      cmp = range(o) - range(this);
-    }
-    if (cmp == 0) {
-      cmp = group(this).compareTo(group(o));
-    }
-    return cmp;
-  }
-
-  private static int action(PermissionRule a) {
-    switch (a.getAction()) {
-      case DENY:
-        return 0;
-      case ALLOW:
-      case BATCH:
-      case BLOCK:
-      case INTERACTIVE:
-      default:
-        return 1 + a.getAction().ordinal();
-    }
-  }
-
-  private static int range(PermissionRule a) {
-    return Math.abs(a.getMin()) + Math.abs(a.getMax());
-  }
-
-  private static String group(PermissionRule a) {
-    return a.getGroup().getName() != null ? a.getGroup().getName() : "";
-  }
-
-  @Override
-  public String toString() {
-    return asString(true);
-  }
-
-  public String asString(boolean canUseRange) {
-    StringBuilder r = new StringBuilder();
-
-    switch (getAction()) {
-      case ALLOW:
-        break;
-
-      case DENY:
-        r.append("deny ");
-        break;
-
-      case BLOCK:
-        r.append("block ");
-        break;
-
-      case INTERACTIVE:
-        r.append("interactive ");
-        break;
-
-      case BATCH:
-        r.append("batch ");
-        break;
-    }
-
-    if (getForce()) {
-      r.append("+force ");
-    }
-
-    if (canUseRange && (getMin() != 0 || getMax() != 0)) {
-      if (0 <= getMin()) {
-        r.append('+');
-      }
-      r.append(getMin());
-      r.append("..");
-      if (0 <= getMax()) {
-        r.append('+');
-      }
-      r.append(getMax());
-      r.append(' ');
-    }
-
-    r.append(getGroup().toConfigValue());
-
-    return r.toString();
-  }
-
-  public static PermissionRule fromString(String src, boolean mightUseRange) {
-    final String orig = src;
-    final PermissionRule rule = new PermissionRule();
-
-    src = src.trim();
-
-    if (src.startsWith("deny ")) {
-      rule.setAction(Action.DENY);
-      src = src.substring("deny ".length()).trim();
-
-    } else if (src.startsWith("block ")) {
-      rule.setAction(Action.BLOCK);
-      src = src.substring("block ".length()).trim();
-
-    } else if (src.startsWith("interactive ")) {
-      rule.setAction(Action.INTERACTIVE);
-      src = src.substring("interactive ".length()).trim();
-
-    } else if (src.startsWith("batch ")) {
-      rule.setAction(Action.BATCH);
-      src = src.substring("batch ".length()).trim();
-    }
-
-    if (src.startsWith("+force ")) {
-      rule.setForce(true);
-      src = src.substring("+force ".length()).trim();
-    }
-
-    if (mightUseRange && !GroupReference.isGroupReference(src)) {
-      int sp = src.indexOf(' ');
-      String range = src.substring(0, sp);
-
-      if (range.matches("^([+-]?\\d+)\\.\\.([+-]?\\d+)$")) {
-        int dotdot = range.indexOf("..");
-        int min = parseInt(range.substring(0, dotdot));
-        int max = parseInt(range.substring(dotdot + 2));
-        rule.setRange(min, max);
-      } else {
-        throw new IllegalArgumentException("Invalid range in rule: " + orig);
-      }
-
-      src = src.substring(sp + 1).trim();
-    }
-
-    String groupName = GroupReference.extractGroupName(src);
-    if (groupName != null) {
-      GroupReference group = new GroupReference();
-      group.setName(groupName);
-      rule.setGroup(group);
-    } else {
-      throw new IllegalArgumentException("Rule must include group: " + orig);
-    }
-
-    return rule;
-  }
-
-  public boolean hasRange() {
-    return getMin() != 0 || getMax() != 0;
-  }
-
-  public static int parseInt(String value) {
-    if (value.startsWith("+")) {
-      value = value.substring(1);
-    }
-    return Integer.parseInt(value);
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof PermissionRule)) {
-      return false;
-    }
-    final PermissionRule other = (PermissionRule) obj;
-    return action.equals(other.action)
-        && force == other.force
-        && min == other.min
-        && max == other.max
-        && group.equals(other.group);
-  }
-
-  @Override
-  public int hashCode() {
-    return group.hashCode();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
deleted file mode 100644
index fe5843ad..0000000
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ /dev/null
@@ -1,185 +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.entities.Account;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-
-/** Describes the state and edits required to submit a change. */
-public class SubmitRecord {
-  public static boolean allRecordsOK(Collection<SubmitRecord> in) {
-    if (in == null || in.isEmpty()) {
-      // If the list is null or empty, it means that this Gerrit installation does not
-      // have any form of validation rules.
-      // Hence, the permission system should be used to determine if the change can be merged
-      // or not.
-      return true;
-    }
-
-    // The change can be submitted, unless at least one plugin prevents it.
-    return in.stream().map(SubmitRecord::status).allMatch(SubmitRecord.Status::allowsSubmission);
-  }
-
-  public enum Status {
-    // NOTE: These values are persisted in the index, so deleting or changing
-    // the name of any values requires a schema upgrade.
-
-    /** The change is ready for submission. */
-    OK,
-
-    /** Something is preventing this change from being submitted. */
-    NOT_READY,
-
-    /** The change has been closed. */
-    CLOSED,
-
-    /** The change was submitted bypassing submit rules. */
-    FORCED,
-
-    /**
-     * An internal server error occurred preventing computation.
-     *
-     * <p>Additional detail may be available in {@link SubmitRecord#errorMessage}.
-     */
-    RULE_ERROR;
-
-    private boolean allowsSubmission() {
-      return this == OK || this == FORCED;
-    }
-  }
-
-  public Status status;
-  public List<Label> labels;
-  public List<SubmitRequirement> requirements;
-  public String errorMessage;
-
-  public static class Label {
-    public enum Status {
-      // NOTE: These values are persisted in the index, so deleting or changing
-      // the name of any values requires a schema upgrade.
-
-      /**
-       * This label provides what is necessary for submission.
-       *
-       * <p>If provided, {@link Label#appliedBy} describes the user account that applied this label
-       * to the change.
-       */
-      OK,
-
-      /**
-       * This label prevents the change from being submitted.
-       *
-       * <p>If provided, {@link Label#appliedBy} describes the user account that applied this label
-       * to the change.
-       */
-      REJECT,
-
-      /** The label is required for submission, but has not been satisfied. */
-      NEED,
-
-      /**
-       * The label may be set, but it's neither necessary for submission nor does it block
-       * submission if set.
-       */
-      MAY,
-
-      /**
-       * The label is required for submission, but is impossible to complete. The likely cause is
-       * access has not been granted correctly by the project owner or site administrator.
-       */
-      IMPOSSIBLE
-    }
-
-    public String label;
-    public Status status;
-    public Account.Id appliedBy;
-
-    @Override
-    public String toString() {
-      StringBuilder sb = new StringBuilder();
-      sb.append(label).append(": ").append(status);
-      if (appliedBy != null) {
-        sb.append(" by ").append(appliedBy);
-      }
-      return sb.toString();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof Label) {
-        Label l = (Label) o;
-        return Objects.equals(label, l.label)
-            && Objects.equals(status, l.status)
-            && Objects.equals(appliedBy, l.appliedBy);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(label, status, appliedBy);
-    }
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append(status);
-    if (status == Status.RULE_ERROR && errorMessage != null) {
-      sb.append('(').append(errorMessage).append(')');
-    }
-    sb.append('[');
-    if (labels != null) {
-      String delimiter = "";
-      for (Label label : labels) {
-        sb.append(delimiter).append(label);
-        delimiter = ", ";
-      }
-    }
-    sb.append("],[");
-    if (requirements != null) {
-      String delimiter = "";
-      for (SubmitRequirement requirement : requirements) {
-        sb.append(delimiter).append(requirement);
-        delimiter = ", ";
-      }
-    }
-    sb.append(']');
-    return sb.toString();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof SubmitRecord) {
-      SubmitRecord r = (SubmitRecord) o;
-      return Objects.equals(status, r.status)
-          && Objects.equals(labels, r.labels)
-          && Objects.equals(errorMessage, r.errorMessage)
-          && Objects.equals(requirements, r.requirements);
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(status, labels, errorMessage, requirements);
-  }
-
-  private Status status() {
-    return status;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
deleted file mode 100644
index 2c341bf..0000000
--- a/java/com/google/gerrit/common/data/SubmitRequirement.java
+++ /dev/null
@@ -1,60 +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.common.data;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.CharMatcher;
-
-/** Describes a requirement to submit a change. */
-@AutoValue
-@AutoValue.CopyAnnotations
-public abstract class SubmitRequirement {
-  private static final CharMatcher TYPE_MATCHER =
-      CharMatcher.inRange('a', 'z')
-          .or(CharMatcher.inRange('A', 'Z'))
-          .or(CharMatcher.inRange('0', '9'))
-          .or(CharMatcher.anyOf("-_"));
-
-  @AutoValue.Builder
-  public abstract static class Builder {
-    public abstract Builder setType(String value);
-
-    public abstract Builder setFallbackText(String value);
-
-    public SubmitRequirement build() {
-      SubmitRequirement requirement = autoBuild();
-      checkState(
-          validateType(requirement.type()),
-          "SubmitRequirement's type contains non alphanumerical symbols.");
-      return requirement;
-    }
-
-    abstract SubmitRequirement autoBuild();
-  }
-
-  public abstract String fallbackText();
-
-  public abstract String type();
-
-  public static Builder builder() {
-    return new AutoValue_SubmitRequirement.Builder();
-  }
-
-  private static boolean validateType(String type) {
-    return TYPE_MATCHER.matchesAllOf(type);
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/java/com/google/gerrit/common/data/SubmitTypeRecord.java
deleted file mode 100644
index afb3bac..0000000
--- a/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ /dev/null
@@ -1,77 +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.data;
-
-import com.google.gerrit.extensions.client.SubmitType;
-
-/** Describes the submit type for a change. */
-public class SubmitTypeRecord {
-  public enum Status {
-    /** The type was computed successfully */
-    OK,
-
-    /**
-     * An internal server error occurred preventing computation.
-     *
-     * <p>Additional detail may be available in {@link SubmitTypeRecord#errorMessage}
-     */
-    RULE_ERROR
-  }
-
-  public static SubmitTypeRecord OK(SubmitType type) {
-    return new SubmitTypeRecord(Status.OK, type, null);
-  }
-
-  public static SubmitTypeRecord error(String err) {
-    return new SubmitTypeRecord(SubmitTypeRecord.Status.RULE_ERROR, null, err);
-  }
-
-  /** Status enum value of the record. */
-  public final Status status;
-
-  /** Submit type of the record; never null if {@link #status} is {@code OK}. */
-  public final SubmitType type;
-
-  /** Submit type of the record; always null if {@link #status} is {@code OK}. */
-  public final String errorMessage;
-
-  private SubmitTypeRecord(Status status, SubmitType type, String errorMessage) {
-    if (type == SubmitType.INHERIT) {
-      throw new IllegalArgumentException("Cannot output submit type " + type);
-    }
-    this.status = status;
-    this.type = type;
-    this.errorMessage = errorMessage;
-  }
-
-  public boolean isOk() {
-    return status == Status.OK;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append(status);
-    if (status == Status.RULE_ERROR && errorMessage != null) {
-      sb.append(" (").append(errorMessage).append(")");
-    }
-    if (type != null) {
-      sb.append('[');
-      sb.append(type.name());
-      sb.append(']');
-    }
-    return sb.toString();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubscribeSection.java b/java/com/google/gerrit/common/data/SubscribeSection.java
deleted file mode 100644
index 6ac4695..0000000
--- a/java/com/google/gerrit/common/data/SubscribeSection.java
+++ /dev/null
@@ -1,107 +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.common.data;
-
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.Project;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import org.eclipse.jgit.transport.RefSpec;
-
-/** Portion of a {@link Project} describing superproject subscription rules. */
-public class SubscribeSection {
-
-  private final List<RefSpec> multiMatchRefSpecs;
-  private final List<RefSpec> matchingRefSpecs;
-  private final Project.NameKey project;
-
-  public SubscribeSection(Project.NameKey p) {
-    project = p;
-    matchingRefSpecs = new ArrayList<>();
-    multiMatchRefSpecs = new ArrayList<>();
-  }
-
-  public void addMatchingRefSpec(RefSpec spec) {
-    matchingRefSpecs.add(spec);
-  }
-
-  public void addMatchingRefSpec(String spec) {
-    RefSpec r = new RefSpec(spec);
-    matchingRefSpecs.add(r);
-  }
-
-  public void addMultiMatchRefSpec(String spec) {
-    RefSpec r = new RefSpec(spec, RefSpec.WildcardMode.ALLOW_MISMATCH);
-    multiMatchRefSpecs.add(r);
-  }
-
-  public Project.NameKey getProject() {
-    return project;
-  }
-
-  /**
-   * Determines if the <code>branch</code> could trigger a superproject update as allowed via this
-   * subscribe section.
-   *
-   * @param branch the branch to check
-   * @return if the branch could trigger a superproject update
-   */
-  public boolean appliesTo(BranchNameKey branch) {
-    for (RefSpec r : matchingRefSpecs) {
-      if (r.matchSource(branch.branch())) {
-        return true;
-      }
-    }
-    for (RefSpec r : multiMatchRefSpecs) {
-      if (r.matchSource(branch.branch())) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public Collection<RefSpec> getMatchingRefSpecs() {
-    return Collections.unmodifiableCollection(matchingRefSpecs);
-  }
-
-  public Collection<RefSpec> getMultiMatchRefSpecs() {
-    return Collections.unmodifiableCollection(multiMatchRefSpecs);
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder ret = new StringBuilder();
-    ret.append("[SubscribeSection, project=");
-    ret.append(project);
-    if (!matchingRefSpecs.isEmpty()) {
-      ret.append(", matching=[");
-      for (RefSpec r : matchingRefSpecs) {
-        ret.append(r.toString());
-        ret.append(", ");
-      }
-    }
-    if (!multiMatchRefSpecs.isEmpty()) {
-      ret.append(", all=[");
-      for (RefSpec r : multiMatchRefSpecs) {
-        ret.append(r.toString());
-        ret.append(", ");
-      }
-    }
-    ret.append("]");
-    return ret.toString();
-  }
-}
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index d841aa6..beb62b4 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -20,8 +20,8 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 
 public class GroupReferenceSubject extends Subject {
 
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index c6400df..47fa383 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,9 +18,7 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V7_6("7.6.*"),
-  V7_7("7.7.*"),
-  V7_8("7.8.*");
+  V7_16("7.16.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/java/com/google/gerrit/entities/AccessSection.java b/java/com/google/gerrit/entities/AccessSection.java
new file mode 100644
index 0000000..d97bca8
--- /dev/null
+++ b/java/com/google/gerrit/entities/AccessSection.java
@@ -0,0 +1,165 @@
+// 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.entities;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/** Portion of a {@link Project} describing access rules. */
+@AutoValue
+public abstract 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/*";
+
+  /** 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 = "^";
+
+  /** Name of the access section. It could be a ref pattern or something else. */
+  public abstract String getName();
+
+  public abstract ImmutableList<Permission> getPermissions();
+
+  public static AccessSection create(String name) {
+    return builder(name).build();
+  }
+
+  public static Builder builder(String name) {
+    return new AutoValue_AccessSection.Builder().setName(name).setPermissions(ImmutableList.of());
+  }
+
+  /** @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/");
+  }
+
+  @Nullable
+  public Permission getPermission(String name) {
+    requireNonNull(name);
+    for (Permission p : getPermissions()) {
+      if (p.getName().equalsIgnoreCase(name)) {
+        return p;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public final int compareTo(AccessSection o) {
+    return comparePattern().compareTo(o.comparePattern());
+  }
+
+  private String comparePattern() {
+    if (getName().startsWith(REGEX_PREFIX)) {
+      return getName().substring(REGEX_PREFIX.length());
+    }
+    return getName();
+  }
+
+  @Override
+  public final String toString() {
+    return "AccessSection[" + getName() + "]";
+  }
+
+  public Builder toBuilder() {
+    Builder b = autoToBuilder();
+    b.getPermissions().stream().map(Permission::toBuilder).forEach(p -> b.addPermission(p));
+    return b;
+  }
+
+  protected abstract Builder autoToBuilder();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    private final List<Permission.Builder> permissionBuilders;
+
+    protected Builder() {
+      permissionBuilders = new ArrayList<>();
+    }
+
+    public abstract Builder setName(String name);
+
+    public abstract String getName();
+
+    public Builder modifyPermissions(Consumer<List<Permission.Builder>> modification) {
+      modification.accept(permissionBuilders);
+      return this;
+    }
+
+    public Builder addPermission(Permission.Builder permission) {
+      requireNonNull(permission, "permission must be non-null");
+      return modifyPermissions(p -> p.add(permission));
+    }
+
+    public Builder remove(Permission.Builder permission) {
+      requireNonNull(permission, "permission must be non-null");
+      return removePermission(permission.getName());
+    }
+
+    public Builder removePermission(String name) {
+      requireNonNull(name, "name must be non-null");
+      return modifyPermissions(
+          p -> p.removeIf(permissionBuilder -> name.equalsIgnoreCase(permissionBuilder.getName())));
+    }
+
+    public Permission.Builder upsertPermission(String permissionName) {
+      requireNonNull(permissionName, "permissionName must be non-null");
+
+      Optional<Permission.Builder> maybePermission =
+          permissionBuilders.stream()
+              .filter(p -> p.getName().equalsIgnoreCase(permissionName))
+              .findAny();
+      if (maybePermission.isPresent()) {
+        return maybePermission.get();
+      }
+
+      Permission.Builder permission = Permission.builder(permissionName);
+      modifyPermissions(p -> p.add(permission));
+      return permission;
+    }
+
+    public AccessSection build() {
+      setPermissions(
+          permissionBuilders.stream().map(Permission.Builder::build).collect(toImmutableList()));
+      if (getPermissions().size()
+          > getPermissions().stream()
+              .map(Permission::getName)
+              .map(String::toLowerCase)
+              .distinct()
+              .count()) {
+        throw new IllegalArgumentException("duplicate permissions: " + getPermissions());
+      }
+      return autoBuild();
+    }
+
+    protected abstract AccessSection autoBuild();
+
+    protected abstract ImmutableList<Permission> getPermissions();
+
+    abstract Builder setPermissions(ImmutableList<Permission> permissions);
+  }
+}
diff --git a/java/com/google/gerrit/entities/AccountsSection.java b/java/com/google/gerrit/entities/AccountsSection.java
new file mode 100644
index 0000000..93083a2
--- /dev/null
+++ b/java/com/google/gerrit/entities/AccountsSection.java
@@ -0,0 +1,28 @@
+// 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.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+@AutoValue
+public abstract class AccountsSection {
+  public abstract ImmutableList<PermissionRule> getSameGroupVisibility();
+
+  public static AccountsSection create(List<PermissionRule> sameGroupVisibility) {
+    return new AutoValue_AccountsSection(ImmutableList.copyOf(sameGroupVisibility));
+  }
+}
diff --git a/java/com/google/gerrit/entities/Address.java b/java/com/google/gerrit/entities/Address.java
new file mode 100644
index 0000000..2324330
--- /dev/null
+++ b/java/com/google/gerrit/entities/Address.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+
+/** Represents an address (name + email) in an email message. */
+@AutoValue
+public abstract class Address {
+  public static Address parse(String in) {
+    final int lt = in.indexOf('<');
+    final int gt = in.indexOf('>');
+    final int at = in.indexOf("@");
+    if (0 <= lt && lt < gt && lt + 1 < at && at + 1 < gt) {
+      final String email = in.substring(lt + 1, gt).trim();
+      final String name = in.substring(0, lt).trim();
+      int nameStart = 0;
+      int nameEnd = name.length();
+      if (name.startsWith("\"")) {
+        nameStart++;
+      }
+      if (name.endsWith("\"")) {
+        nameEnd--;
+      }
+      return Address.create(name.length() > 0 ? name.substring(nameStart, nameEnd) : null, email);
+    }
+
+    if (lt < 0 && gt < 0 && 0 < at && at < in.length() - 1) {
+      return Address.create(in);
+    }
+
+    throw new IllegalArgumentException("Invalid email address: " + in);
+  }
+
+  public static Address tryParse(String in) {
+    try {
+      return parse(in);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  public static Address create(String email) {
+    return create(null, email);
+  }
+
+  public static Address create(String name, String email) {
+    return new AutoValue_Address(name, email);
+  }
+
+  @Nullable
+  public abstract String name();
+
+  public abstract String email();
+
+  @Override
+  public final int hashCode() {
+    return email().hashCode();
+  }
+
+  @Override
+  public final boolean equals(Object other) {
+    if (other instanceof Address) {
+      return email().equals(((Address) other).email());
+    }
+    return false;
+  }
+
+  @Override
+  public final String toString() {
+    return toHeaderString();
+  }
+
+  public String toHeaderString() {
+    if (name() != null) {
+      return quotedPhrase(name()) + " <" + email() + ">";
+    } else if (isSimple()) {
+      return email();
+    }
+    return "<" + email() + ">";
+  }
+
+  private static final String MUST_QUOTE_EMAIL = "()<>,;:\\\"[]";
+  private static final String MUST_QUOTE_NAME = MUST_QUOTE_EMAIL + "@.";
+
+  private boolean isSimple() {
+    for (int i = 0; i < email().length(); i++) {
+      final char c = email().charAt(i);
+      if (c <= ' ' || 0x7F <= c || MUST_QUOTE_EMAIL.indexOf(c) != -1) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static String quotedPhrase(String name) {
+    if (EmailHeader.needsQuotedPrintable(name)) {
+      return EmailHeader.quotedPrintable(name);
+    }
+    for (int i = 0; i < name.length(); i++) {
+      final char c = name.charAt(i);
+      if (MUST_QUOTE_NAME.indexOf(c) != -1) {
+        return wrapInQuotes(name);
+      }
+    }
+    return name;
+  }
+
+  private static String wrapInQuotes(String name) {
+    final StringBuilder r = new StringBuilder(2 + name.length());
+    r.append('"');
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if (c == '"' || c == '\\') {
+        r.append('\\');
+      }
+      r.append(c);
+    }
+    r.append('"');
+    return r.toString();
+  }
+}
diff --git a/java/com/google/gerrit/entities/AttentionSetUpdate.java b/java/com/google/gerrit/entities/AttentionSetUpdate.java
index 45588722..2e58608 100644
--- a/java/com/google/gerrit/entities/AttentionSetUpdate.java
+++ b/java/com/google/gerrit/entities/AttentionSetUpdate.java
@@ -16,6 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import java.time.Instant;
 
 /**
@@ -23,9 +24,7 @@
  * in reverse chronological order. Since each update contains all required information and
  * invalidates all previous state, only the most recent record is relevant for each user.
  *
- * <p>See {@link com.google.gerrit.extensions.api.changes.AddToAttentionSetInput} and {@link
- * com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput} for the representation in
- * the API.
+ * <p>See {@link AttentionSetInput} for the representation in the API.
  */
 @AutoValue
 public abstract class AttentionSetUpdate {
diff --git a/java/com/google/gerrit/entities/BUILD b/java/com/google/gerrit/entities/BUILD
index 26265ae..66d1869 100644
--- a/java/com/google/gerrit/entities/BUILD
+++ b/java/com/google/gerrit/entities/BUILD
@@ -16,6 +16,7 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/errorprone:annotations",
+        "//lib/flogger:api",
         "//proto:cache_java_proto",
         "//proto:entities_java_proto",
     ],
diff --git a/java/com/google/gerrit/entities/BranchOrderSection.java b/java/com/google/gerrit/entities/BranchOrderSection.java
new file mode 100644
index 0000000..f964e59
--- /dev/null
+++ b/java/com/google/gerrit/entities/BranchOrderSection.java
@@ -0,0 +1,62 @@
+// 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.entities;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/**
+ * An ordering of branches by stability.
+ *
+ * <p>The REST API supports automatically checking if changes on development branches can be merged
+ * into stable branches. This is configured by the {@code branchOrder.branch} project setting. This
+ * class represents the ordered list of branches, by increasing stability.
+ */
+@AutoValue
+public abstract class BranchOrderSection {
+
+  /**
+   * Branch names ordered from least to the most stable.
+   *
+   * <p>Typically the order will be like: master, stable-M.N, stable-M.N-1, ...
+   *
+   * <p>Ref names in this list are exactly as they appear in {@code project.config}
+   */
+  public abstract ImmutableList<String> order();
+
+  public static BranchOrderSection create(Collection<String> order) {
+    // Do not mutate the given list as this will be written back to disk when ProjectConfig is
+    // stored.
+    return new AutoValue_BranchOrderSection(ImmutableList.copyOf(order));
+  }
+
+  /**
+   * Returns the tail list of branches that are more stable - so lower in the entire list ordered by
+   * priority compared to the provided branch. Always returns a fully qualified ref name (including
+   * the refs/heads/ prefix).
+   */
+  public ImmutableList<String> getMoreStable(String branch) {
+    ImmutableList<String> fullyQualifiedOrder =
+        order().stream().map(RefNames::fullName).collect(toImmutableList());
+    int i = fullyQualifiedOrder.indexOf(RefNames.fullName(branch));
+    if (0 <= i) {
+      return fullyQualifiedOrder.subList(i + 1, fullyQualifiedOrder.size());
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/java/com/google/gerrit/entities/CachedProjectConfig.java b/java/com/google/gerrit/entities/CachedProjectConfig.java
new file mode 100644
index 0000000..0b755b7
--- /dev/null
+++ b/java/com/google/gerrit/entities/CachedProjectConfig.java
@@ -0,0 +1,273 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import static com.google.common.base.Preconditions.checkState;
+
+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 java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Cached representation of values parsed from {@link
+ * com.google.gerrit.server.project.ProjectConfig}.
+ *
+ * <p>This class is immutable and thread-safe.
+ */
+@AutoValue
+public abstract class CachedProjectConfig {
+  public abstract Project getProject();
+
+  public abstract ImmutableMap<AccountGroup.UUID, GroupReference> getGroups();
+
+  /** Returns a set of all groups used by this configuration. */
+  public ImmutableSet<AccountGroup.UUID> getAllGroupUUIDs() {
+    return getGroups().keySet();
+  }
+
+  /**
+   * Returns the group reference for a {@link AccountGroup.UUID}, if the group is used by at least
+   * one rule.
+   */
+  public Optional<GroupReference> getGroup(AccountGroup.UUID uuid) {
+    return Optional.ofNullable(getGroups().get(uuid));
+  }
+
+  /**
+   * Returns the group reference for matching the given {@code name}, if the group is used by at
+   * least one rule.
+   */
+  public Optional<GroupReference> getGroupByName(@Nullable String name) {
+    if (name == null) {
+      return Optional.empty();
+    }
+    return getGroups().values().stream().filter(g -> name.equals(g.getName())).findAny();
+  }
+
+  /** Returns the account section containing visibility information about accounts. */
+  public abstract AccountsSection getAccountsSection();
+
+  /** Returns a map of {@link AccessSection}s keyed by their name. */
+  public abstract ImmutableMap<String, AccessSection> getAccessSections();
+
+  /** Returns the {@link AccessSection} with to the given name. */
+  public Optional<AccessSection> getAccessSection(String refName) {
+    return Optional.ofNullable(getAccessSections().get(refName));
+  }
+
+  /** Returns all {@link AccessSection} names. */
+  public ImmutableSet<String> getAccessSectionNames() {
+    return ImmutableSet.copyOf(getAccessSections().keySet());
+  }
+
+  /**
+   * Returns the {@link BranchOrderSection} containing the order in which branches should be shown.
+   */
+  public abstract Optional<BranchOrderSection> getBranchOrderSection();
+
+  /** Returns the {@link ContributorAgreement}s keyed by their name. */
+  public abstract ImmutableMap<String, ContributorAgreement> getContributorAgreements();
+
+  /** Returns the {@link NotifyConfig}s keyed by their name. */
+  public abstract ImmutableMap<String, NotifyConfig> getNotifySections();
+
+  /** Returns the {@link LabelType}s keyed by their name. */
+  public abstract ImmutableMap<String, LabelType> getLabelSections();
+
+  /** Returns configured {@link ConfiguredMimeTypes}s. */
+  public abstract ConfiguredMimeTypes getMimeTypes();
+
+  /** Returns {@link SubscribeSection} keyed by the {@link Project.NameKey} they reference. */
+  public abstract ImmutableMap<Project.NameKey, SubscribeSection> getSubscribeSections();
+
+  /** Returns {@link StoredCommentLinkInfo} keyed by their name. */
+  public abstract ImmutableMap<String, StoredCommentLinkInfo> getCommentLinkSections();
+
+  /** Returns the blob ID of the {@code rules.pl} file, if present. */
+  public abstract Optional<ObjectId> getRulesId();
+
+  // TODO(hiesel): This should not have to be an Optional.
+  /** Returns the SHA1 of the {@code refs/meta/config} branch. */
+  public abstract Optional<ObjectId> getRevision();
+
+  /** Returns the maximum allowed object size. */
+  public abstract long getMaxObjectSizeLimit();
+
+  /** Returns {@code true} if received objects should be checked for validity. */
+  public abstract boolean getCheckReceivedObjects();
+
+  /** Returns a list of panel sections keyed by title. */
+  public abstract ImmutableMap<String, ImmutableList<String>> getExtensionPanelSections();
+
+  public ImmutableList<SubscribeSection> getSubscribeSections(BranchNameKey branch) {
+    return filterSubscribeSectionsByBranch(getSubscribeSections().values(), branch);
+  }
+
+  public abstract ImmutableMap<String, String> getPluginConfigs();
+
+  /**
+   * Returns the {@link Config} that got parsed from the specified {@code fileName} on {@code
+   * refs/meta/config}. The returned instance is a defensive copy of the cached value.
+   *
+   * @param fileName the name of the file. Must end in {@code .config}.
+   * @return an {@link Optional} of the {@link Config}. {@link Optional#empty()} if the file was not
+   *     found or could not be parsed. {@link com.google.gerrit.server.project.ProjectConfig} will
+   *     surface validation errors in case of a parsing issue.
+   */
+  public Optional<Config> getProjectLevelConfig(String fileName) {
+    checkState(fileName.endsWith(".config"), "file name must end in .config");
+    if (getProjectLevelConfigs().containsKey(fileName)) {
+      Config config = new Config();
+      try {
+        config.fromText(getProjectLevelConfigs().get(fileName));
+      } catch (ConfigInvalidException e) {
+        // This is OK to propagate as IllegalStateException because it's a programmer error.
+        // The config was converted to a String using Config#toText. So #fromText must not
+        // throw a ConfigInvalidException
+        throw new IllegalStateException("invalid config for " + fileName, e);
+      }
+      return Optional.of(config);
+    }
+    return Optional.empty();
+  }
+
+  public abstract ImmutableMap<String, String> getProjectLevelConfigs();
+
+  public static Builder builder() {
+    return new AutoValue_CachedProjectConfig.Builder();
+  }
+
+  public abstract Builder toBuilder();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setProject(Project value);
+
+    public abstract Builder setAccountsSection(AccountsSection value);
+
+    public abstract Builder setBranchOrderSection(Optional<BranchOrderSection> value);
+
+    public Builder addGroup(GroupReference groupReference) {
+      groupsBuilder().put(groupReference.getUUID(), groupReference);
+      return this;
+    }
+
+    public Builder addAccessSection(AccessSection accessSection) {
+      accessSectionsBuilder().put(accessSection.getName(), accessSection);
+      return this;
+    }
+
+    public Builder addContributorAgreement(ContributorAgreement contributorAgreement) {
+      contributorAgreementsBuilder().put(contributorAgreement.getName(), contributorAgreement);
+      return this;
+    }
+
+    public Builder addNotifySection(NotifyConfig notifyConfig) {
+      notifySectionsBuilder().put(notifyConfig.getName(), notifyConfig);
+      return this;
+    }
+
+    public Builder addLabelSection(LabelType labelType) {
+      labelSectionsBuilder().put(labelType.getName(), labelType);
+      return this;
+    }
+
+    public abstract Builder setMimeTypes(ConfiguredMimeTypes value);
+
+    public Builder addSubscribeSection(SubscribeSection subscribeSection) {
+      subscribeSectionsBuilder().put(subscribeSection.project(), subscribeSection);
+      return this;
+    }
+
+    public Builder addCommentLinkSection(StoredCommentLinkInfo storedCommentLinkInfo) {
+      commentLinkSectionsBuilder().put(storedCommentLinkInfo.getName(), storedCommentLinkInfo);
+      return this;
+    }
+
+    public abstract Builder setRulesId(Optional<ObjectId> value);
+
+    public abstract Builder setRevision(Optional<ObjectId> value);
+
+    public abstract Builder setMaxObjectSizeLimit(long value);
+
+    public abstract Builder setCheckReceivedObjects(boolean value);
+
+    public abstract ImmutableMap.Builder<String, ImmutableList<String>>
+        extensionPanelSectionsBuilder();
+
+    public Builder setExtensionPanelSections(Map<String, List<String>> value) {
+      value
+          .entrySet()
+          .forEach(
+              e ->
+                  extensionPanelSectionsBuilder()
+                      .put(e.getKey(), ImmutableList.copyOf(e.getValue())));
+      return this;
+    }
+
+    abstract ImmutableMap.Builder<String, String> pluginConfigsBuilder();
+
+    public Builder addPluginConfig(String pluginName, String pluginConfig) {
+      pluginConfigsBuilder().put(pluginName, pluginConfig);
+      return this;
+    }
+
+    abstract ImmutableMap.Builder<String, String> projectLevelConfigsBuilder();
+
+    public Builder addProjectLevelConfig(String configFileName, String config) {
+      projectLevelConfigsBuilder().put(configFileName, config);
+      return this;
+    }
+
+    public abstract CachedProjectConfig build();
+
+    protected abstract ImmutableMap.Builder<AccountGroup.UUID, GroupReference> groupsBuilder();
+
+    protected abstract ImmutableMap.Builder<String, AccessSection> accessSectionsBuilder();
+
+    protected abstract ImmutableMap.Builder<String, ContributorAgreement>
+        contributorAgreementsBuilder();
+
+    protected abstract ImmutableMap.Builder<String, NotifyConfig> notifySectionsBuilder();
+
+    protected abstract ImmutableMap.Builder<String, LabelType> labelSectionsBuilder();
+
+    protected abstract ImmutableMap.Builder<Project.NameKey, SubscribeSection>
+        subscribeSectionsBuilder();
+
+    protected abstract ImmutableMap.Builder<String, StoredCommentLinkInfo>
+        commentLinkSectionsBuilder();
+  }
+
+  private static ImmutableList<SubscribeSection> filterSubscribeSectionsByBranch(
+      Collection<SubscribeSection> allSubscribeSections, BranchNameKey branch) {
+    ImmutableList.Builder<SubscribeSection> ret = ImmutableList.builder();
+    for (SubscribeSection s : allSubscribeSections) {
+      if (s.appliesTo(branch)) {
+        ret.add(s);
+      }
+    }
+    return ret.build();
+  }
+}
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index b36b5f9..845a9bb 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -39,7 +39,7 @@
  *          |
  *          +- {@link PatchSetApproval}: a +/- vote on the change's current state.
  *          |
- *          +- {@link Comment}: comment about a specific line
+ *          +- {@link HumanComment}: comment about a specific line
  * </pre>
  *
  * <p>
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 9c58fef..37b8620 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -24,15 +24,15 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
- * This class represents inline comments in NoteDb. This means it determines the JSON format for
- * inline comments in the revision notes that NoteDb uses to persist inline comments.
+ * This class is a base class that can be extended by the different types of inline comment
+ * entities.
  *
  * <p>Changing fields in this class changes the storage format of inline comments in NoteDb and may
  * require a corresponding data migration (adding new optional fields is generally okay).
  *
- * <p>Consider updating {@link #getApproximateSize()} when adding/changing fields.
+ * <p>Consider updating {@link #getCommentFieldApproximateSize()} when adding/changing fields.
  */
-public class Comment {
+public abstract class Comment {
   public enum Status {
     DRAFT('d'),
 
@@ -231,7 +231,6 @@
   private String revId;
 
   public String serverId;
-  public boolean unresolved;
 
   public Comment(Comment c) {
     this(
@@ -240,14 +239,13 @@
         new Timestamp(c.writtenOn.getTime()),
         c.side,
         c.message,
-        c.serverId,
-        c.unresolved);
+        c.serverId);
     this.lineNbr = c.lineNbr;
     this.realAuthor = c.realAuthor;
+    this.parentUuid = c.parentUuid;
     this.range = c.range != null ? new Range(c.range) : null;
     this.tag = c.tag;
     this.revId = c.revId;
-    this.unresolved = c.unresolved;
   }
 
   public Comment(
@@ -256,8 +254,7 @@
       Timestamp writtenOn,
       short side,
       String message,
-      String serverId,
-      boolean unresolved) {
+      String serverId) {
     this.key = key;
     this.author = new Comment.Identity(author);
     this.realAuthor = this.author;
@@ -265,7 +262,6 @@
     this.side = side;
     this.message = message;
     this.serverId = serverId;
-    this.unresolved = unresolved;
   }
 
   public void setLineNbrAndRange(
@@ -301,11 +297,13 @@
    * Returns the comment's approximate size. This is used to enforce size limits and should
    * therefore include all unbounded fields (e.g. String-s).
    */
-  public int getApproximateSize() {
+  protected int getCommentFieldApproximateSize() {
     return nullableLength(message, parentUuid, tag, revId, serverId)
         + (key != null ? nullableLength(key.filename, key.uuid) : 0);
   }
 
+  public abstract int getApproximateSize();
+
   static int nullableLength(String... strings) {
     int length = 0;
     for (String s : strings) {
@@ -331,8 +329,7 @@
         && Objects.equals(range, c.range)
         && Objects.equals(tag, c.tag)
         && Objects.equals(revId, c.revId)
-        && Objects.equals(serverId, c.serverId)
-        && unresolved == c.unresolved;
+        && Objects.equals(serverId, c.serverId);
   }
 
   @Override
@@ -349,8 +346,7 @@
         range,
         tag,
         revId,
-        serverId,
-        unresolved);
+        serverId);
   }
 
   @Override
@@ -370,7 +366,6 @@
         .add("parentUuid", Objects.toString(parentUuid, ""))
         .add("range", Objects.toString(range, ""))
         .add("revId", Objects.toString(revId, ""))
-        .add("tag", Objects.toString(tag, ""))
-        .add("unresolved", unresolved);
+        .add("tag", Objects.toString(tag, ""));
   }
 }
diff --git a/java/com/google/gerrit/entities/ConfiguredMimeTypes.java b/java/com/google/gerrit/entities/ConfiguredMimeTypes.java
new file mode 100644
index 0000000..6ba89c9
--- /dev/null
+++ b/java/com/google/gerrit/entities/ConfiguredMimeTypes.java
@@ -0,0 +1,154 @@
+// 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.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.fnmatch.FileNameMatcher;
+import org.eclipse.jgit.lib.Config;
+
+@AutoValue
+public abstract class ConfiguredMimeTypes {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String MIMETYPE = "mimetype";
+  private static final String KEY_PATH = "path";
+
+  public abstract ImmutableList<TypeMatcher> matchers();
+
+  public static ConfiguredMimeTypes create(String projectName, Config rc) {
+    Set<String> types = rc.getSubsections(MIMETYPE);
+    ImmutableList.Builder<TypeMatcher> matchers = ImmutableList.builder();
+    if (!types.isEmpty()) {
+      for (String typeName : types) {
+        for (String path : rc.getStringList(MIMETYPE, typeName, KEY_PATH)) {
+          try {
+            if (path.startsWith("^")) {
+              matchers.add(new ReType(typeName, path));
+            } else {
+              matchers.add(new FnType(typeName, path));
+            }
+          } catch (PatternSyntaxException | InvalidPatternException e) {
+            logger.atWarning().log(
+                "Ignoring invalid %s.%s.%s = %s in project %s: %s",
+                MIMETYPE, typeName, KEY_PATH, path, projectName, e.getMessage());
+          }
+        }
+      }
+    }
+    return new AutoValue_ConfiguredMimeTypes(matchers.build());
+  }
+
+  public static ConfiguredMimeTypes create(ImmutableList<TypeMatcher> matchers) {
+    return new AutoValue_ConfiguredMimeTypes(matchers);
+  }
+
+  @Nullable
+  public String getMimeType(String path) {
+    for (TypeMatcher m : matchers()) {
+      if (m.matches(path)) {
+        return m.type;
+      }
+    }
+    return null;
+  }
+
+  public abstract static class TypeMatcher {
+    private final String type;
+    private final String pattern;
+
+    private TypeMatcher(String type, String pattern) {
+      this.type = type;
+      this.pattern = pattern;
+    }
+
+    public String getPattern() {
+      return pattern;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    protected abstract boolean matches(String path);
+  }
+
+  public static class FnType extends TypeMatcher {
+    private final FileNameMatcher matcher;
+
+    public FnType(String type, String pattern) throws InvalidPatternException {
+      super(type, pattern);
+      this.matcher = new FileNameMatcher(pattern, null);
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      FileNameMatcher m = new FileNameMatcher(matcher);
+      m.append(input);
+      return m.isMatch();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof FnType)) {
+        return false;
+      }
+      FnType other = (FnType) o;
+      return Objects.equals(other.getType(), getType())
+          && Objects.equals(other.getPattern(), getPattern());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(getType(), getPattern());
+    }
+  }
+
+  public static class ReType extends TypeMatcher {
+    private final Pattern re;
+
+    public ReType(String type, String pattern) throws PatternSyntaxException {
+      super(type, pattern);
+      this.re = Pattern.compile(pattern);
+    }
+
+    @Override
+    protected boolean matches(String input) {
+      return re.matcher(input).matches();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof ReType)) {
+        return false;
+      }
+      ReType other = (ReType) o;
+      return Objects.equals(other.getType(), getType())
+          && Objects.equals(other.getPattern(), getPattern());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(getType(), getPattern());
+    }
+  }
+}
diff --git a/java/com/google/gerrit/entities/ContributorAgreement.java b/java/com/google/gerrit/entities/ContributorAgreement.java
new file mode 100644
index 0000000..1d933b5
--- /dev/null
+++ b/java/com/google/gerrit/entities/ContributorAgreement.java
@@ -0,0 +1,80 @@
+// 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.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import java.util.List;
+
+/** Portion of a {@link Project} describing a single contributor agreement. */
+@AutoValue
+public abstract class ContributorAgreement implements Comparable<ContributorAgreement> {
+  public abstract String getName();
+
+  @Nullable
+  public abstract String getDescription();
+
+  public abstract ImmutableList<PermissionRule> getAccepted();
+
+  @Nullable
+  public abstract GroupReference getAutoVerify();
+
+  @Nullable
+  public abstract String getAgreementUrl();
+
+  public abstract ImmutableList<String> getExcludeProjectsRegexes();
+
+  public abstract ImmutableList<String> getMatchProjectsRegexes();
+
+  public static ContributorAgreement.Builder builder(String name) {
+    return new AutoValue_ContributorAgreement.Builder()
+        .setName(name)
+        .setAccepted(ImmutableList.of())
+        .setExcludeProjectsRegexes(ImmutableList.of())
+        .setMatchProjectsRegexes(ImmutableList.of());
+  }
+
+  @Override
+  public final int compareTo(ContributorAgreement o) {
+    return getName().compareTo(o.getName());
+  }
+
+  @Override
+  public final String toString() {
+    return "ContributorAgreement[" + getName() + "]";
+  }
+
+  public abstract Builder toBuilder();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setName(String name);
+
+    public abstract Builder setDescription(@Nullable String description);
+
+    public abstract Builder setAccepted(ImmutableList<PermissionRule> accepted);
+
+    public abstract Builder setAutoVerify(@Nullable GroupReference autoVerify);
+
+    public abstract Builder setAgreementUrl(@Nullable String agreementUrl);
+
+    public abstract Builder setExcludeProjectsRegexes(List<String> excludeProjectsRegexes);
+
+    public abstract Builder setMatchProjectsRegexes(List<String> matchProjectsRegexes);
+
+    public abstract ContributorAgreement build();
+  }
+}
diff --git a/java/com/google/gerrit/entities/EmailHeader.java b/java/com/google/gerrit/entities/EmailHeader.java
new file mode 100644
index 0000000..71414c7
--- /dev/null
+++ b/java/com/google/gerrit/entities/EmailHeader.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+public abstract class EmailHeader {
+  public abstract boolean isEmpty();
+
+  public abstract void write(Writer w) throws IOException;
+
+  public static class String extends EmailHeader {
+    private final java.lang.String value;
+
+    public String(java.lang.String v) {
+      value = v;
+    }
+
+    public java.lang.String getString() {
+      return value;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return value == null || value.length() == 0;
+    }
+
+    @Override
+    public void write(Writer w) throws IOException {
+      if (needsQuotedPrintable(value)) {
+        w.write(quotedPrintable(value));
+      } else {
+        w.write(value);
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof String) && Objects.equals(value, ((String) o).value);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(value).toString();
+    }
+  }
+
+  public static boolean needsQuotedPrintable(java.lang.String value) {
+    for (int i = 0; i < value.length(); i++) {
+      if (value.charAt(i) < ' ' || '~' < value.charAt(i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  static boolean needsQuotedPrintableWithinPhrase(int cp) {
+    switch (cp) {
+      case '!':
+      case '*':
+      case '+':
+      case '-':
+      case '/':
+      case '=':
+      case '_':
+        return false;
+      default:
+        if (('a' <= cp && cp <= 'z') || ('A' <= cp && cp <= 'Z') || ('0' <= cp && cp <= '9')) {
+          return false;
+        }
+        return true;
+    }
+  }
+
+  public static java.lang.String quotedPrintable(java.lang.String value) {
+    final StringBuilder r = new StringBuilder();
+
+    r.append("=?UTF-8?Q?");
+    for (int i = 0; i < value.length(); i++) {
+      final int cp = value.codePointAt(i);
+      if (cp == ' ') {
+        r.append('_');
+
+      } else if (needsQuotedPrintableWithinPhrase(cp)) {
+        byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes(UTF_8);
+        for (byte b : buf) {
+          r.append('=');
+          r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase());
+          r.append(Integer.toHexString(b & 0x0f).toUpperCase());
+        }
+
+      } else {
+        r.append(Character.toChars(cp));
+      }
+    }
+    r.append("?=");
+
+    return r.toString();
+  }
+
+  public static class Date extends EmailHeader {
+    private final java.util.Date value;
+
+    public Date(java.util.Date v) {
+      value = v;
+    }
+
+    public java.util.Date getDate() {
+      return value;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return value == null;
+    }
+
+    @Override
+    public void write(Writer w) throws IOException {
+      final SimpleDateFormat fmt;
+      // Mon, 1 Jun 2009 10:49:44 -0700
+      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US);
+      w.write(fmt.format(value));
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof Date) && Objects.equals(value, ((Date) o).value);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(value).toString();
+    }
+  }
+
+  public static class AddressList extends EmailHeader {
+    private final List<Address> list = new ArrayList<>();
+
+    public AddressList() {}
+
+    public AddressList(Address addr) {
+      add(addr);
+    }
+
+    public List<Address> getAddressList() {
+      return Collections.unmodifiableList(list);
+    }
+
+    public void add(Address addr) {
+      list.add(addr);
+    }
+
+    public void remove(java.lang.String email) {
+      list.removeIf(address -> address.email().equals(email));
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return list.isEmpty();
+    }
+
+    @Override
+    public void write(Writer w) throws IOException {
+      int len = 8;
+      boolean firstAddress = true;
+      boolean needComma = false;
+      for (Address addr : list) {
+        java.lang.String s = addr.toHeaderString();
+        if (firstAddress) {
+          firstAddress = false;
+        } else if (72 < len + s.length()) {
+          w.write(",\r\n\t");
+          len = 8;
+          needComma = false;
+        }
+
+        if (needComma) {
+          w.write(", ");
+        }
+        w.write(s);
+        len += s.length();
+        needComma = true;
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(list);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof AddressList) && Objects.equals(list, ((AddressList) o).list);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(list).toString();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/entities/GroupDescription.java b/java/com/google/gerrit/entities/GroupDescription.java
new file mode 100644
index 0000000..e950257
--- /dev/null
+++ b/java/com/google/gerrit/entities/GroupDescription.java
@@ -0,0 +1,66 @@
+// 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.entities;
+
+import com.google.gerrit.common.Nullable;
+import java.sql.Timestamp;
+import java.util.Set;
+
+/** Group methods exposed by the GroupBackend. */
+public class GroupDescription {
+  /** The Basic information required to be exposed by any Group. */
+  public interface Basic {
+    /** @return the non-null UUID of the group. */
+    AccountGroup.UUID getGroupUUID();
+
+    /** @return the non-null name of the group. */
+    String getName();
+
+    /**
+     * @return optional email address to send to the group's members. If provided, Gerrit will use
+     *     this email address to send change notifications to the group.
+     */
+    @Nullable
+    String getEmailAddress();
+
+    /**
+     * @return optional URL to information about the group. Typically a URL to a web page that
+     *     permits users to apply to join the group, or manage their membership.
+     */
+    @Nullable
+    String getUrl();
+  }
+
+  /** The extended information exposed by internal groups. */
+  public interface Internal extends Basic {
+
+    AccountGroup.Id getId();
+
+    @Nullable
+    String getDescription();
+
+    AccountGroup.UUID getOwnerGroupUUID();
+
+    boolean isVisibleToAll();
+
+    Timestamp getCreatedOn();
+
+    Set<Account.Id> getMembers();
+
+    Set<AccountGroup.UUID> getSubgroups();
+  }
+
+  private GroupDescription() {}
+}
diff --git a/java/com/google/gerrit/entities/GroupReference.java b/java/com/google/gerrit/entities/GroupReference.java
new file mode 100644
index 0000000..208ba0f
--- /dev/null
+++ b/java/com/google/gerrit/entities/GroupReference.java
@@ -0,0 +1,99 @@
+// 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.entities;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+
+/** Describes a group within a projects {@link AccessSection}s. */
+@AutoValue
+public abstract class GroupReference implements Comparable<GroupReference> {
+
+  private static final String PREFIX = "group ";
+
+  public static GroupReference forGroup(GroupDescription.Basic group) {
+    return GroupReference.create(group.getGroupUUID(), group.getName());
+  }
+
+  public static boolean isGroupReference(String configValue) {
+    return configValue != null && configValue.startsWith(PREFIX);
+  }
+
+  @Nullable
+  public static String extractGroupName(String configValue) {
+    if (!isGroupReference(configValue)) {
+      return null;
+    }
+    return configValue.substring(PREFIX.length()).trim();
+  }
+
+  @Nullable
+  public abstract AccountGroup.UUID getUUID();
+
+  public abstract String getName();
+
+  /**
+   * Create a group reference.
+   *
+   * @param uuid UUID of the group, must not be {@code null}
+   * @param name the group name, must not be {@code null}
+   */
+  public static GroupReference create(AccountGroup.UUID uuid, String name) {
+    return new AutoValue_GroupReference(requireNonNull(uuid), requireNonNull(name));
+  }
+
+  /**
+   * Create a group reference where the group's name couldn't be resolved.
+   *
+   * @param name the group name, must not be {@code null}
+   */
+  public static GroupReference create(String name) {
+    return new AutoValue_GroupReference(null, name);
+  }
+
+  @Override
+  public final int compareTo(GroupReference o) {
+    return uuid(this).compareTo(uuid(o));
+  }
+
+  private static String uuid(GroupReference a) {
+    if (a.getUUID() != null && a.getUUID().get() != null) {
+      return a.getUUID().get();
+    }
+
+    return "?";
+  }
+
+  @Override
+  public final int hashCode() {
+    return uuid(this).hashCode();
+  }
+
+  @Override
+  public final boolean equals(Object o) {
+    return o instanceof GroupReference && compareTo((GroupReference) o) == 0;
+  }
+
+  @Override
+  public final String toString() {
+    return "Group[" + getName() + " / " + getUUID() + "]";
+  }
+
+  public String toConfigValue() {
+    return PREFIX + getName();
+  }
+}
diff --git a/java/com/google/gerrit/entities/HumanComment.java b/java/com/google/gerrit/entities/HumanComment.java
new file mode 100644
index 0000000..50bee8d
--- /dev/null
+++ b/java/com/google/gerrit/entities/HumanComment.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import java.sql.Timestamp;
+import java.util.Objects;
+
+/**
+ * This class represents inline human comments in NoteDb. This means it determines the JSON format
+ * for inline comments in the revision notes that NoteDb uses to persist inline comments.
+ *
+ * <p>Changing fields in this class changes the storage format of inline comments in NoteDb and may
+ * require a corresponding data migration (adding new optional fields is generally okay).
+ *
+ * <p>Consider updating {@link #getApproximateSize()} when adding/changing fields.
+ */
+public class HumanComment extends Comment {
+
+  public boolean unresolved;
+
+  public HumanComment(
+      Key key,
+      Account.Id author,
+      Timestamp writtenOn,
+      short side,
+      String message,
+      String serverId,
+      boolean unresolved) {
+    super(key, author, writtenOn, side, message, serverId);
+    this.unresolved = unresolved;
+  }
+
+  public HumanComment(HumanComment comment) {
+    super(comment);
+  }
+
+  @Override
+  public int getApproximateSize() {
+    return super.getCommentFieldApproximateSize();
+  }
+
+  @Override
+  public String toString() {
+    return toStringHelper().add("unresolved", unresolved).toString();
+  }
+
+  @Override
+  public boolean equals(Object otherObject) {
+    if (!(otherObject instanceof HumanComment)) {
+      return false;
+    }
+    if (!super.equals(otherObject)) {
+      return false;
+    }
+    HumanComment otherComment = (HumanComment) otherObject;
+    return unresolved == otherComment.unresolved;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), unresolved);
+  }
+}
diff --git a/java/com/google/gerrit/entities/LabelFunction.java b/java/com/google/gerrit/entities/LabelFunction.java
new file mode 100644
index 0000000..f361741
--- /dev/null
+++ b/java/com/google/gerrit/entities/LabelFunction.java
@@ -0,0 +1,123 @@
+// 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.entities;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.SubmitRecord.Label;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Functions for determining submittability based on label votes.
+ *
+ * <p>Only describes built-in label functions. Admins can extend the logic arbitrarily using Prolog
+ * rules, in which case the choice of function in the project config is ignored.
+ *
+ * <p>Function semantics are documented in {@code config-labels.txt}, and actual behavior is
+ * implemented both in Prolog in {@code gerrit_common.pl} and in the {@link #check} method.
+ */
+public enum LabelFunction {
+  ANY_WITH_BLOCK("AnyWithBlock", true, false, false),
+  MAX_WITH_BLOCK("MaxWithBlock", true, true, true),
+  MAX_NO_BLOCK("MaxNoBlock", false, true, true),
+  NO_BLOCK("NoBlock"),
+  NO_OP("NoOp"),
+  PATCH_SET_LOCK("PatchSetLock");
+
+  public static final Map<String, LabelFunction> ALL;
+
+  static {
+    Map<String, LabelFunction> all = new LinkedHashMap<>();
+    for (LabelFunction f : values()) {
+      all.put(f.getFunctionName(), f);
+    }
+    ALL = Collections.unmodifiableMap(all);
+  }
+
+  public static Optional<LabelFunction> parse(@Nullable String str) {
+    return Optional.ofNullable(ALL.get(str));
+  }
+
+  private final String name;
+  private final boolean isBlock;
+  private final boolean isRequired;
+  private final boolean requiresMaxValue;
+
+  LabelFunction(String name) {
+    this(name, false, false, false);
+  }
+
+  LabelFunction(String name, boolean isBlock, boolean isRequired, boolean requiresMaxValue) {
+    this.name = name;
+    this.isBlock = isBlock;
+    this.isRequired = isRequired;
+    this.requiresMaxValue = requiresMaxValue;
+  }
+
+  /** The function name as defined in documentation and {@code project.config}. */
+  public String getFunctionName() {
+    return name;
+  }
+
+  /** Whether the label is a "block" label, meaning a minimum vote will prevent submission. */
+  public boolean isBlock() {
+    return isBlock;
+  }
+
+  /** Whether the label is a mandatory label, meaning absence of votes will prevent submission. */
+  public boolean isRequired() {
+    return isRequired;
+  }
+
+  /** Whether the label requires a vote with the maximum value to allow submission. */
+  public boolean isMaxValueRequired() {
+    return requiresMaxValue;
+  }
+
+  public Label check(LabelType labelType, Iterable<PatchSetApproval> approvals) {
+    Label submitRecordLabel = new Label();
+    submitRecordLabel.label = labelType.getName();
+
+    submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
+    if (isRequired) {
+      submitRecordLabel.status = SubmitRecord.Label.Status.NEED;
+    }
+
+    for (PatchSetApproval a : approvals) {
+      if (a.value() == 0) {
+        continue;
+      }
+
+      if (isBlock && labelType.isMaxNegative(a)) {
+        submitRecordLabel.appliedBy = a.accountId();
+        submitRecordLabel.status = SubmitRecord.Label.Status.REJECT;
+        return submitRecordLabel;
+      }
+
+      if (labelType.isMaxPositive(a) || !requiresMaxValue) {
+        submitRecordLabel.appliedBy = a.accountId();
+
+        submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
+        if (isRequired) {
+          submitRecordLabel.status = SubmitRecord.Label.Status.OK;
+        }
+      }
+    }
+
+    return submitRecordLabel;
+  }
+}
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
new file mode 100644
index 0000000..a8d4da5
--- /dev/null
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -0,0 +1,298 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@AutoValue
+public abstract class LabelType {
+  public static final boolean DEF_ALLOW_POST_SUBMIT = true;
+  public static final boolean DEF_CAN_OVERRIDE = true;
+  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CHANGE = true;
+  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false;
+  public static final boolean DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = false;
+  public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
+  public static final boolean DEF_COPY_ANY_SCORE = false;
+  public static final boolean DEF_COPY_MAX_SCORE = false;
+  public static final boolean DEF_COPY_MIN_SCORE = false;
+  public static final ImmutableList<Short> DEF_COPY_VALUES = ImmutableList.of();
+  public static final boolean DEF_IGNORE_SELF_APPROVAL = false;
+
+  public static LabelType withDefaultValues(String name) {
+    checkName(name);
+    List<LabelValue> values = new ArrayList<>(2);
+    values.add(LabelValue.create((short) 0, "Rejected"));
+    values.add(LabelValue.create((short) 1, "Approved"));
+    return create(name, values);
+  }
+
+  public static String checkName(String name) throws IllegalArgumentException {
+    checkNameInternal(name);
+    if ("SUBM".equals(name)) {
+      throw new IllegalArgumentException("Reserved label name \"" + name + "\"");
+    }
+    return name;
+  }
+
+  public static String checkNameInternal(String name) throws IllegalArgumentException {
+    if (name == null || name.isEmpty()) {
+      throw new IllegalArgumentException("Empty label name");
+    }
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if ((i == 0 && c == '-')
+          || !((c >= 'a' && c <= 'z')
+              || (c >= 'A' && c <= 'Z')
+              || (c >= '0' && c <= '9')
+              || c == '-')) {
+        throw new IllegalArgumentException("Illegal label name \"" + name + "\"");
+      }
+    }
+    return name;
+  }
+
+  private static ImmutableList<LabelValue> sortValues(List<LabelValue> values) {
+    if (values.isEmpty()) {
+      return ImmutableList.of();
+    }
+    values = values.stream().sorted(comparing(LabelValue::getValue)).collect(toList());
+    short v = values.get(0).getValue();
+    short i = 0;
+    ImmutableList.Builder<LabelValue> result = ImmutableList.builder();
+    // Fill in any missing values with empty text.
+    while (i < values.size()) {
+      while (v < values.get(i).getValue()) {
+        result.add(LabelValue.create(v++, ""));
+      }
+      v++;
+      result.add(values.get(i++));
+    }
+    return result.build();
+  }
+
+  public abstract String getName();
+
+  public abstract LabelFunction getFunction();
+
+  public abstract boolean isCopyAnyScore();
+
+  public abstract boolean isCopyMinScore();
+
+  public abstract boolean isCopyMaxScore();
+
+  public abstract boolean isCopyAllScoresOnMergeFirstParentUpdate();
+
+  public abstract boolean isCopyAllScoresOnTrivialRebase();
+
+  public abstract boolean isCopyAllScoresIfNoCodeChange();
+
+  public abstract boolean isCopyAllScoresIfNoChange();
+
+  public abstract ImmutableList<Short> getCopyValues();
+
+  public abstract boolean isAllowPostSubmit();
+
+  public abstract boolean isIgnoreSelfApproval();
+
+  public abstract short getDefaultValue();
+
+  public abstract ImmutableList<LabelValue> getValues();
+
+  public abstract short getMaxNegative();
+
+  public abstract short getMaxPositive();
+
+  public abstract boolean isCanOverride();
+
+  @Nullable
+  public abstract ImmutableList<String> getRefPatterns();
+
+  public abstract ImmutableMap<Short, LabelValue> getByValue();
+
+  public static LabelType create(String name, List<LabelValue> valueList) {
+    return LabelType.builder(name, valueList).build();
+  }
+
+  public static LabelType.Builder builder(String name, List<LabelValue> valueList) {
+    return (new AutoValue_LabelType.Builder())
+        .setName(name)
+        .setValues(valueList)
+        .setDefaultValue((short) 0)
+        .setFunction(LabelFunction.MAX_WITH_BLOCK)
+        .setMaxNegative(Short.MIN_VALUE)
+        .setMaxPositive(Short.MAX_VALUE)
+        .setCanOverride(DEF_CAN_OVERRIDE)
+        .setCopyAllScoresIfNoChange(DEF_COPY_ALL_SCORES_IF_NO_CHANGE)
+        .setCopyAllScoresIfNoCodeChange(DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE)
+        .setCopyAllScoresOnTrivialRebase(DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE)
+        .setCopyAllScoresOnMergeFirstParentUpdate(DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE)
+        .setCopyAnyScore(DEF_COPY_ANY_SCORE)
+        .setCopyMaxScore(DEF_COPY_MAX_SCORE)
+        .setCopyMinScore(DEF_COPY_MIN_SCORE)
+        .setCopyValues(DEF_COPY_VALUES)
+        .setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT)
+        .setIgnoreSelfApproval(DEF_IGNORE_SELF_APPROVAL);
+  }
+
+  public boolean matches(PatchSetApproval psa) {
+    return psa.labelId().get().equalsIgnoreCase(getName());
+  }
+
+  public LabelValue getMin() {
+    if (getValues().isEmpty()) {
+      return null;
+    }
+    return getValues().get(0);
+  }
+
+  public LabelValue getMax() {
+    if (getValues().isEmpty()) {
+      return null;
+    }
+    return getValues().get(getValues().size() - 1);
+  }
+
+  public boolean isMaxNegative(PatchSetApproval ca) {
+    return getMaxNegative() == ca.value();
+  }
+
+  public boolean isMaxPositive(PatchSetApproval ca) {
+    return getMaxPositive() == ca.value();
+  }
+
+  public LabelValue getValue(short value) {
+    return getByValue().get(value);
+  }
+
+  public LabelValue getValue(PatchSetApproval ca) {
+    return getByValue().get(ca.value());
+  }
+
+  public LabelId getLabelId() {
+    return LabelId.create(getName());
+  }
+
+  @Override
+  public final String toString() {
+    StringBuilder sb = new StringBuilder(getName()).append('[');
+    LabelValue min = getMin();
+    LabelValue max = getMax();
+    if (min != null && max != null) {
+      sb.append(
+          new PermissionRange(Permission.forLabel(getName()), min.getValue(), max.getValue())
+              .toString()
+              .trim());
+    } else if (min != null) {
+      sb.append(min.formatValue().trim());
+    } else if (max != null) {
+      sb.append(max.formatValue().trim());
+    }
+    sb.append(']');
+    return sb.toString();
+  }
+
+  public abstract Builder toBuilder();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setName(String name);
+
+    public abstract Builder setFunction(LabelFunction function);
+
+    public abstract Builder setCanOverride(boolean canOverride);
+
+    public abstract Builder setAllowPostSubmit(boolean allowPostSubmit);
+
+    public abstract Builder setIgnoreSelfApproval(boolean ignoreSelfApproval);
+
+    public abstract Builder setRefPatterns(@Nullable List<String> refPatterns);
+
+    public abstract Builder setValues(List<LabelValue> values);
+
+    public abstract Builder setDefaultValue(short defaultValue);
+
+    public abstract Builder setCopyAnyScore(boolean copyAnyScore);
+
+    public abstract Builder setCopyMinScore(boolean copyMinScore);
+
+    public abstract Builder setCopyMaxScore(boolean copyMaxScore);
+
+    public abstract Builder setCopyAllScoresOnMergeFirstParentUpdate(
+        boolean copyAllScoresOnMergeFirstParentUpdate);
+
+    public abstract Builder setCopyAllScoresOnTrivialRebase(boolean copyAllScoresOnTrivialRebase);
+
+    public abstract Builder setCopyAllScoresIfNoCodeChange(boolean copyAllScoresIfNoCodeChange);
+
+    public abstract Builder setCopyAllScoresIfNoChange(boolean copyAllScoresIfNoChange);
+
+    public abstract Builder setCopyValues(Collection<Short> copyValues);
+
+    public abstract Builder setMaxNegative(short maxNegative);
+
+    public abstract Builder setMaxPositive(short maxPositive);
+
+    public abstract ImmutableList<LabelValue> getValues();
+
+    protected abstract String getName();
+
+    protected abstract ImmutableList<Short> getCopyValues();
+
+    protected abstract Builder setByValue(ImmutableMap<Short, LabelValue> byValue);
+
+    @Nullable
+    protected abstract ImmutableList<String> getRefPatterns();
+
+    protected abstract LabelType autoBuild();
+
+    public LabelType build() throws IllegalArgumentException {
+      setName(checkName(getName()));
+      if (getRefPatterns() == null || getRefPatterns().isEmpty()) {
+        // Empty to null
+