Merge branch 'stable-3.9' into stable-3.10

* stable-3.9:
  Fix NPE upon Git clone
  Fix javadocs for deprecation of Changes#id(int)

Release-Notes: skip
Change-Id: Ib33c0d1c4f884cabba86f7d0d9c2929104d1a5da
diff --git a/.bazelproject b/.bazelproject
index ad7b022..0f2ff90 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -16,7 +16,7 @@
 targets:
   //...:all
 
-java_language_level: 11
+java_language_level: 17
 
 workspace_type: java
 
diff --git a/.bazelrc b/.bazelrc
index a8f1210..480bea7 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,3 +1,7 @@
+# TODO(davido): Migrate all dependencies from WORKSPACE to MODULE.bazel
+# https://issues.gerritcodereview.com/issues/303819949
+common --noenable_bzlmod
+
 build --workspace_status_command="python3 ./tools/workspace_status.py"
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --action_env=PATH
@@ -28,24 +32,28 @@
 build:remote_bb --config=config_bb
 build:remote_bb --config=build_shared
 
-# Define configuration using remotejdk_11, executes using remotejdk_11 or local_jdk
-build:build_java11_shared --java_language_version=11
-build:build_java11_shared --java_runtime_version=remotejdk_11
-build:build_java11_shared --tool_java_language_version=11
-build:build_java11_shared --tool_java_runtime_version=remotejdk_11
+# Builds using remotejdk_21, executes using remotejdk_21 or local_jdk
+build:build_java21_shared --java_language_version=21
+build:build_java21_shared --java_runtime_version=remotejdk_21
+build:build_java21_shared --tool_java_language_version=21
+build:build_java21_shared --tool_java_runtime_version=remotejdk_21
 
-build:java11 --config=build_java11_shared
+build:java21 --config=build_java21_shared
 
-# Builds and executes on Google GCP RBE using remotejdk_11
-build:remote11 --config=config_gcp
-build:remote11 --config=build_java11_shared
+# Builds and executes on RBE using remotejdk_21
+build:remote21 --config=config_gcp
+build:remote21 --config=build_java21_shared
 
-# Define remote11 configuration alias
-build:remote11_gcp --config=remote11
+# Define remote21 configuration alias
+build:remote21_gcp --config=remote21
 
 # Builds and executes on BuildBuddy RBE using remotejdk_11
-build:remote11_bb --config=config_bb
-build:remote11_bb --config=build_java11_shared
+build:remote21_bb --config=config_bb
+build:remote21_bb --config=build_java21_shared
+
+# Enable modern C++ features
+build --cxxopt=-std=c++17
+build --host_cxxopt=-std=c++17
 
 # Enable strict_action_env flag to. For more information on this feature see
 # https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
@@ -58,9 +66,8 @@
 
 test --build_tests_only
 test --test_output=errors
-# This option is the default for Bazel 7 that is used on master since
-# change Ie7cb3003d, so this additional config should be removed when
-# merging to master.
-test --incompatible_sandbox_hermetic_tmp
 
 import %workspace%/tools/remote-bazelrc
+
+# User-specific .bazelrc
+try-import %workspace%/user.bazelrc
diff --git a/.bazelversion b/.bazelversion
index 91e4a9f..66ce77b 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-6.3.2
+7.0.0
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 19a19dd..2e90cff 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -264,6 +264,7 @@
 `-2..+2`, as the user's membership of `Foo Leads` effectively grant
 them access to the entire reference space, thanks to the wildcard.
 
+[[exclusive]]
 Gerrit also supports exclusive reference-level access control.
 
 It is possible to configure Gerrit to grant an exclusive ref level
@@ -443,13 +444,13 @@
 
 
 [[category_create]]
-=== Create Reference
+=== Create (aka Create Reference)
 
-The create reference category controls whether it is possible to
-create new references, branches or tags.  This implies that the
-reference must not already exist, it's not a destructive permission
-in that you can't overwrite or remove any previously existing
-references (and also discard any commits in the process).
+The create category controls whether it is possible to create new
+references, branches or tags.  This implies that the reference must not
+already exist, it's not a destructive permission in that you can't
+overwrite or remove any previously existing references (and also
+discard any commits in the process).
 
 It's probably most common to either permit the creation of a single
 branch in many gits (by granting permission on a parent project), or
@@ -484,9 +485,9 @@
 `${username}` are still reachable by the users.
 
 [[category_delete]]
-=== Delete Reference
+=== Delete (aka Delete Reference)
 
-The delete reference category controls whether it is possible to delete
+The delete category controls whether it is possible to delete
 references, branches or tags. It doesn't allow any other update of
 references.
 
@@ -550,29 +551,46 @@
 [[category_owner]]
 === Owner
 
-The `Owner` category controls which groups can modify the project's
-configuration.  Users who are members of an owner group can:
+The `Owner` category on `refs/*` controls which groups own the project,
+i.e. the users who are members of an owner group are called the
+`project owners`.
 
-* Change the project description
-* Grant/revoke any access rights, including `Owner`
+Project owners can change the link:config-project-config.html[project
+configuration], including:
 
-To get SSH branch access project owners must grant an access right to a group
-they are a member of, just like for any other user.
+* Granting/revoking any access rights (including the `Owner` access
+  right)
+* Changing the project description
+* Changing the link to the parent project
+* Changing the project options
+* Changing link:config-labels.html[labels]
+* Changing link:config-submit-requirements.html[submit requirements]
 
-Ownership over a particular branch subspace may be delegated by
-entering a branch pattern.  To delegate control over all branches
-that begin with `qa/` to the QA group, add `Owner` category
-for reference `+refs/heads/qa/*+`.  Members of the QA group can
-further refine access, but only for references that begin with
-`refs/heads/qa/`. See <<project_owners,project owners>> to find
-out more about this role.
+[NOTE]
+Access rights that are assigned to the magic
+link:#project_owners[Project Owners] group are resolved to the users
+that are project owners by having the `Owner` permission assigned on
+`refs/*`.
 
+[NOTE]
+To get branch access via Git project owners must grant an access right
+to a group they are a member of, just like for any other user.
+
+[NOTE]
 For the `All-Projects` root project any `Owner` access right on
 'refs/*' is ignored since this permission would allow users to edit the
 global capabilities, which is the same as being able to administrate
 the Gerrit server (e.g. the user could assign the `Administrate Server`
 capability to the own account).
 
+Ownership over a particular branch subspace may be delegated by
+entering a branch pattern. E.g. to delegate control over all branches
+that begin with `qa/` to the QA group, add the `Owner` category
+for reference `+refs/heads/qa/*+`. Members of the QA group can
+further refine access, but only for references that begin with
+`refs/heads/qa/`. See <<project_owners,project owners>> to find
+out more about this role.
+
 
 [[category_push]]
 === Push
@@ -1618,6 +1636,7 @@
 
 Access to refs can be blocked, allowed or denied.
 
+[[block-rule]]
 ==== BLOCK
 
 For blocking access, all rules marked BLOCK are tested, and if one
@@ -1643,6 +1662,7 @@
 permissions when they are processed in the ALLOW/DENY processing, as
 described in the next subsection.
 
+[[allow-rule]]
 ==== ALLOW
 
 For allowing access, all ALLOW/DENY rules that might apply to a ref
@@ -1656,6 +1676,7 @@
 This ordering lets project owners apply permissions specific to their
 project, overwriting the site defaults specified in All-Projects.
 
+[[deny-rule]]
 ==== DENY
 
 DENY is processed together with ALLOW.
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index e547822..ccb6d58 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -56,6 +56,12 @@
 The `Change-Id` will not be added if `gerrit.createChangeId` is set
 to `false` in the git config.
 
+The `Change-Id` will not be added to temporary commits created by
+`git commit --fixup` or `git commit --squash`, as well as commits
+with a subject line that begins with a lowercase word followed by
+an exclamation mark (e.g., `nopush!`). To override this behavior,
+set `gerrit.createChangeId` to `always` 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`
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index dd7fa02..c54181b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -116,7 +116,11 @@
 current user is a member of are visible.
 +
 If `VISIBLE_GROUP`, only users who are members of at least one group
-that is visible to the current user are visible.
+that is visible to the current user are visible. To make an account
+visible to all users (e.g. because it is a service account) when
+`VISIBLE_GROUP` is used, create a `Public Users` group that has the
+`Make group visible to all registered users` option enabled and add the
+account as a group member.
 +
 If `NONE`, no users other than the current user are visible.
 +
@@ -526,10 +530,10 @@
 
 [[auth.cookieSecure]]auth.cookieSecure::
 +
-Sets "secure" flag of the authentication cookie.  If true, cookies
+Sets "secure" flag of the authentication cookie.  If `true`, cookies
 will be transmitted only over HTTPS protocol.
 +
-By default, false.
+By default, `false`.
 
 [[auth.emailFormat]]auth.emailFormat::
 +
@@ -558,7 +562,7 @@
 `'$site_path'/static`, so users can actually complete one or
 more agreements.
 +
-By default this is false (no agreements are used).
+By default this is `false` (no agreements are used).
 +
 To enable the actual usage of contributor agreement the project
 specific config option in the `project.config` must be set:
@@ -567,14 +571,14 @@
 
 [[auth.trustContainerAuth]]auth.trustContainerAuth::
 +
-If true then it is the responsibility of the container hosting
+If `true` then it is the responsibility of the container hosting
 Gerrit to authenticate users. In this case Gerrit will blindly trust
 the container.
 +
 This parameter only affects git over http traffic. If set to false
 then Gerrit will do the authentication (using Basic authentication).
 +
-By default this is set to false.
+By default this is set to `false`.
 
 
 [[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
@@ -652,7 +656,7 @@
 +
 This parameter only affects git over http and git over SSH traffic.
 +
-By default this is set to false.
+By default this is set to `false`.
 
 [[auth.userNameCaseInsensitive]]auth.userNameCaseInsensitive::
 +
@@ -679,29 +683,29 @@
 note name would be identical and thus conflict. These duplicates thus
 have to be deleted manually by deleting the respective external ID.
 +
-For newly initialized sites this option defaults to true.
+For newly initialized sites this option defaults to `true`.
 +
-Default is false.
+Default is `false`.
 
 [[auth.userNameCaseInsensitiveMigrationMode]]auth.userNameCaseInsensitiveMigrationMode::
 +
-Setting migration mode to true allows to fallback to case sensitive
+Setting migration mode to `true` allows to fallback to case sensitive
 behaviour if the migrated external ID cannot be found. This allows to
 trigger the migration while Gerrit process is running.
 +
-Default is false.
+Default is `false`.
 
 [[auth.enableRunAs]]auth.enableRunAs::
 +
-If true HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
+If `true` HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
 header from any users granted the link:access-control.html#capability_runAs[Run As]
 capability. The header and capability permit the authenticated user
 to impersonate another account.
 +
-If false the feature is disabled and cannot be re-enabled without
+If `false` the feature is disabled and cannot be re-enabled without
 editing gerrit.config and restarting the server.
 +
-Default is true.
+Default is `true`.
 
 [[auth.allowRegisterNewEmail]]auth.allowRegisterNewEmail::
 +
@@ -711,13 +715,13 @@
 link:#auth.httpemailheader[auth.httpemailheader] must *not* be set to
 enable registration of new email addresses.
 +
-By default, true.
+By default, `true`.
 
 [[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,
+If set to `true`, upon login, if the authentication back-end reports the account as 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
@@ -725,12 +729,12 @@
 should ensure that their authentication back-end is supported. Currently, only
 strict 'LDAP' authentication is supported.
 +
-In addition, if this parameter is not set, or false, the corresponding scheduled
+In addition, if this parameter is not set, or `false`, the corresponding scheduled
 task to deactivate inactive Gerrit accounts will also be disabled. If this
-parameter is set to true, users should also consider configuring the
+parameter is set to `true`, users should also consider configuring the
 link:#accountDeactivation[accountDeactivation] section appropriately.
 +
-By default, false.
+By default, `false`.
 
 [[auth.skipFullRefEvaluationIfAllRefsAreVisible]]auth.skipFullRefEvaluationIfAllRefsAreVisible::
 +
@@ -740,7 +744,7 @@
 The full ref filtering would filter out refs for pending edits, private changes
 and auto merge commits.
 +
-By default, true.
+By default, `true`.
 
 [[cache]]
 === Section cache
@@ -777,7 +781,7 @@
 Whether to enable the computation of disk statistics of persistent caches.
 This computation is expensive and requires a long time on larger installations.
 +
-By default, false.
+By default, `false`.
 
 [[cache.h2CacheSize]]cache.h2CacheSize::
 +
@@ -800,13 +804,13 @@
 
 [[cache.h2AutoServer]]cache.h2AutoServer::
 +
-If set to true, enable H2 autoserver mode for the H2-backed persistent cache
+If set to `true`, enable H2 autoserver mode for the H2-backed persistent cache
 databases.
 +
 See link:http://www.h2database.com/html/features.html#auto_mixed_mode[here,role=external,window=_blank]
 for detail.
 +
-Default is false.
+Default is `false`.
 
 [[cache.openFiles]]cache.openFiles::
 +
@@ -1310,7 +1314,7 @@
 this setting will fallback to `cache.diff.intraline` if not set in the
 configuration.
 +
-Default is true, enabled.
+Default is `true`, enabled.
 
 [[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
 +
@@ -1320,12 +1324,12 @@
 size set under <<cache.name.memoryLimit,cache.projects.memoryLimit>>
 is not smaller than the number of repos.
 +
-Default is false, disabled.
+Default is `false`, disabled.
 
 [[cache.projects.loadThreads]]cache.projects.loadThreads::
 +
 Only relevant if <<cache.projects.loadOnStartup,cache.projects.loadOnStartup>>
-is true.
+is `true`.
 +
 The number of threads to allocate for loading the cache at startup. These
 threads will die out after the cache is loaded.
@@ -1348,6 +1352,33 @@
 
 Default is 00:00 if the project_list cache warmer is enabled.
 
+[[cachePruning]]
+=== Section cachePruning
+
+[[cachePruning.pruneOnStartup]]cachePruning.pruneOnStartup::
++
+Whether to asynchronously prune all cache when starting Gerrit.
++
+Defaults to `true`.
+
+[[cachePruning.startTime]]cachePruning.startTime::
++
+The link:#schedule-configuration-startTime[start time] for running
+cache pruning.
++
+Defaults to `01:00`.
+
+[[cachePruning.interval]]cachePruning.interval::
++
+The link:#schedule-configuration-interval[interval] for running
+cache pruning.
++
+Defaults to `1d`.
+
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
+
+
 [[capability]]
 === Section capability
 
@@ -1380,7 +1411,7 @@
 `administrateServer` capability assigned. This is useful to bootstrap
 the link:config-accounts.html[account data].
 +
-Default is true.
+Default is `true`.
 
 
 [[change]]
@@ -1388,9 +1419,9 @@
 
 [[change.allowBlame]]change.allowBlame::
 +
-Allow blame on side by side diff. If set to false, blame cannot be used.
+Allow blame on side by side diff. If set to `false`, blame cannot be used.
 +
-Default is true.
+Default is `true`.
 
 [[change.cacheAutomerge]]change.cacheAutomerge::
 +
@@ -1400,13 +1431,13 @@
 diff caches (`"git_modified_files"`, `modified_files`, `"git_file_diff"`,
 `"file_diff"`).
 +
-If true, automerge results are stored in the repository under
+If `true`, automerge results are stored in the repository under
 `refs/cache-automerge/*`; the results of diffing the change against its
-automerge base are stored in the diff caches. If false, no extra data is
+automerge base are stored in the diff caches. If `false`, no extra data is
 stored in the repository, only the diff caches. This can result in slight
 performance improvements by reducing the number of refs in the repo.
 +
-Default is true.
+Default is `true`.
 
 [[change.commentSizeLimit]]change.commentSizeLimit::
 +
@@ -1434,9 +1465,9 @@
 
 [[change.disablePrivateChanges]]change.disablePrivateChanges::
 +
-If set to true, users are not allowed to create private changes.
+If set to `true`, users are not allowed to create private changes.
 +
-The default is false.
+The default is `false`.
 
 [[change.maxComments]]change.maxComments::
 +
@@ -1527,12 +1558,12 @@
 This setting determines when Gerrit renders conflict changes section on change
 screen and also supports `conflicts` predicate. This computation is expensive,
 computing ConflictsPredicate has a runtime complexity of O(nˆ2) with n number
-of open changes on a branch. When set to false GUI will silently ignore the
+of open changes on a branch. When set to `false` GUI will silently ignore the
 error message and leave the conflict changes section on change screen empty.
 See also implications on rendering of conflict changes section in configuration
 section:link:#change.mergeabilityComputationBehavior[change.mergeabilityComputationBehavior].
 
-Default is true.
+Default is `true`.
 
 [[change.maxSubmittableAtOnce]]change.maxSubmittableAtOnce::
 +
@@ -1555,29 +1586,43 @@
 branch (see details in
 link:https://issues.gerritcodereview.com/issues/40009784[issue 40009784]).
 +
-By default true.
+By default `true`.
 
 [[change.enableRobotComments]]change.enableRobotComments::
 +
 Are robot comments enabled in the Gerrit UI? This setting allows phasing out
 robot comments.
 +
-By default true.
+By default `true`.
 
 [[change.propagateSubmitRequirementErrors]]change.propagateSubmitRequirementErrors::
 +
-If a SubmitRequirement evaluation for a given change results in an
-ERROR status, abort the REST response with an HTTP 500 error.
+If set, requests that access the submit requirements of a change fail with an
+HTTP 500 error if the change has a submit requirement with a non-parseable
+expression that would otherwise result in an
+link:config-submit-requirements#status-error[ERROR] status for the submit
+requirement.
 +
-The ERROR status can occur if a SubmitRequirement uses a
-plugin-provided predicate (and the plugin is not available), due to
-bugs, or due to bypassing the validation that normally happens when
-updating `refs/meta/config`.
+Submit requirement expressions can become non-parseable due to bypassing the
+validation that normally happens when updating the project configuration in
+the `refs/meta/config` branch, or due to bugs in Gerrit.
 +
-Enabling this flag  makes gerrit unusuable under such conditions, so
-it is generally not recommended. However, this makes the
-application-specific ERROR status into a generic HTTP error, and can
-thus be acted on by automated deployment and monitoring infrastructure.
+A special case are expressions that use plugin-provided predicates. If any
+plugin that provides a predicate fails to load (e.g. due to an error in the
+plugin) the predicate can no longer be resolved and expressions that are using
+it can no longer be parsed. This is an error that requires the attention of the
+team that operates Gerrit, but in order to get notified when this happens the
+operation team would need to setup custom monitoring that observes whether
+link:config-submit-requirements#status-error[ERROR] statuses are returned for
+submit requirements. Instead this config option can be used to make
+non-parseable submit requirement expressions cause HTTP 500 errors which
+triggers the automatic alerting for errors that Gerrit operation teams usually
+have in place. This allows the operation team to react quickly when this
+happens.
++
+The drawback of enabling this option is that it causes requests to fail rather
+than handling parsing errors gracefully, which can make Gerrit for impacted
+users unusable.
 
 [[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
 +
@@ -1590,11 +1635,11 @@
 
 [[change.sendNewPatchsetEmails]]change.sendNewPatchsetEmails::
 +
-When false, emails will not be sent to owners, reviewers, and cc for
+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.
+Default is `true`.
 
 [[change.strictLabels]]change.strictLabels::
 +
@@ -1602,7 +1647,7 @@
 configuration option is provided for backwards compatibility and may
 be removed in future gerrit versions.
 +
-Default is false.
+Default is `false`.
 
 [[change.submitLabel]]change.submitLabel::
 +
@@ -1647,7 +1692,7 @@
 
 [[change.submitTopicTooltip]]change.submitTopicTooltip::
 +
-If `change.submitWholeTopic` is configured to true and a change has a
+If `change.submitWholeTopic` is configured to `true` and a change has a
 topic, this configuration determines the tooltip for the submit button
 instead of `change.submitTooltip`. The variable `${topicSize}` is available
 for the number of changes in the same topic to be submitted. The number of
@@ -1662,7 +1707,7 @@
 Determines if the submit button submits the whole topic instead of
 just the current change.
 +
-Default is false.
+Default is `false`.
 
 [[change.updateDelay]]change.updateDelay::
 +
@@ -1691,7 +1736,7 @@
 This setting takes effect when generating the automerge, which happens on upload.
 Changing the setting leaves existing changes unaffected.
 +
-Default is false.
+Default is `false`.
 
 [[change.maxFileSizeDiff]]change.maxFileSizeDiff::
 +
@@ -1700,20 +1745,20 @@
 +
 If not set or set to zero, no limits are applied on file sizes.
 
-[[change.skipCurrentRulesEvaluationOnClosedChanges]]
+[[change.skipCurrentRulesEvaluationOnClosedChanges]]change.skipCurrentRulesEvaluationOnClosedChanges::
 +
-If false, Gerrit will always take latest project configuration to
+If `false`, Gerrit will always take latest project configuration to
 compute submit labels. This means that, closed changes (either merged
 or abandoned) will be evaluated against the latest configuration which
 may produce different results. Especially for merged changes, they may
 look like they didn't meet the submit requirements.
 +
-When true, evaluation will be skipped and Gerrit will show the
+When `true`, evaluation will be skipped and Gerrit will show the
 exact status of submit labels when change was submitted. Post-review
 votes will only be allowed on labels that were configured when change
 was closed.
 +
-Default it false.
+Default is `false`.
 
 [[changeCleanup]]
 === Section changeCleanup
@@ -1844,6 +1889,10 @@
 will hyperlink terms such as 'bug 42' to an external bug tracker,
 supplying the argument record number '42' for display.
 
+Before matching is done the relevant contents are html-escaped. If 'match' needs
+to contain `&`, `<`, `>`, `"` or  `'`, replace them with `&amp;`, `&gt;`,
+`&lt;`, `&quot;` and `&apos;` respectively.
+
 commentlinks supports link:#reloadConfig[configuration reloads]. Though a
 link:cmd-flush-caches.html[flush-caches] of "projects" is needed for the
 commentlinks to be immediately available in the UI.
@@ -1920,7 +1969,7 @@
 section in a parent or the site-wide config that is disabled by
 specifying `enabled = true`.
 +
-By default, true.
+By default, `true`.
 +
 Note that the names and contents of disabled sections are visible even
 to anonymous users via the
@@ -1979,7 +2028,7 @@
 
 [[container.replica]]container.replica::
 +
-Used on Gerrit replica installations. If set to true the Gerrit JVM is
+Used on Gerrit replica installations. If set to `true` the Gerrit JVM is
 called with the '--replica' switch, enabling replica mode. If no value is
 set (or any other value), Gerrit defaults to primary mode enabling write
 operations.
@@ -2112,18 +2161,18 @@
 
 [[core.packedGitMmap]]core.packedGitMmap::
 +
-When true, JGit will use `mmap()` rather than `malloc()+read()`
+When `true`, JGit will use `mmap()` rather than `malloc()+read()`
 to load data from pack files.  The use of mmap can be problematic
 on some JVMs as the garbage collector must deduce that a memory
 mapped segment is no longer in use before a call to `munmap()`
 can be made by the JVM native code.
 +
 In server applications (such as Gerrit) that need to access many
-pack files, setting this to true risks artificially running out
+pack files, setting this to `true` risks artificially running out
 of virtual address space, as the garbage collector cannot reclaim
 unused mapped spaces fast enough.
 +
-Default on JGit is false. Although potentially slower, it yields
+Default on JGit is `false`. Although potentially slower, it yields
 much more predictable behavior.
 
 [[core.asyncLoggingBufferSize]]core.asyncLoggingBufferSize::
@@ -2145,7 +2194,7 @@
 blog,role=external,window=_blank], the recursive merge produces better results if the two commits
 that are merged have more than one common predecessor.
 +
-Default is true.
+Default is `true`.
 
 [[core.repositoryCacheCleanupDelay]]core.repositoryCacheCleanupDelay::
 +
@@ -2182,7 +2231,7 @@
 (lazily) if needed. This helps reduce the overhead of checking if
 the packed-refs file is outdated.
 +
-Default is true.
+Default is `true`.
 
 [[dashboard]]
 === Section dashboard
@@ -2347,7 +2396,7 @@
 collections are more expensive but may lead to significantly smaller
 repositories.
 +
-Valid values are "true" and "false," default is "false".
+Valid values are "`true`" and "`false`," default is "`false`".
 
 [[gc.startTime]]gc.startTime::
 +
@@ -2443,7 +2492,7 @@
 for editing GPG keys. If disabled, GPG keys can only be added by
 administrators with direct git access to All-Users.
 +
-Defaults to true.
+Defaults to `true`.
 
 [[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
 +
@@ -2532,14 +2581,14 @@
 Enable rendering of project list from the secondary index instead
 of purely relying on the in-memory cache.
 +
-By default false.
+By default `false`.
 +
 [NOTE]
-The in-memory cache (set to false) rendering provides an **unlimited list** as a result
+The in-memory cache (set to `false`) rendering provides an **unlimited list** as a result
 of the list project API, causing the full list of projects to be
 returned as a result of the link:rest-api-projects.html[/projects/] REST API
 or the link:cmd-ls-projects.html[gerrit ls-projects] SSH command.
-When the rendering from the secondary index (set to true),
+When the rendering from the secondary index (set to `true`),
 the **list is limited** by the global capability
 link:access-control.html#capability_queryLimit[queryLimit]
 which is defaulted to 500 entries.
@@ -2571,7 +2620,7 @@
 
 Record actual peer IP address in ref log entry for identified user.
 
-Defaults to false.
+Defaults to `false`.
 
 [[gerrit.secureStoreClass]]gerrit.secureStoreClass::
 +
@@ -2586,9 +2635,9 @@
 [[gerrit.canLoadInIFrame]]gerrit.canLoadInIFrame::
 +
 For security reasons Gerrit will always jump out of iframe.
-Setting this option to true will prevent this behavior.
+Setting this option to `true` will prevent this behavior.
 +
-By default false.
+By default `false`.
 
 [[gerrit.xframeOption]]gerrit.xframeOption::
 +
@@ -2602,7 +2651,7 @@
 1. ALLOW - The page can be displayed in a frame.
 2. SAMEORIGIN - The page can only be displayed in a frame on the same origin as the page itself.
 +
-If link:#gerrit.canLoadInIFrame is set to false this option is ignored and the
+If link:#gerrit.canLoadInIFrame is set to `false` this option is ignored and the
 `X-Frame-Options` header is always set to `DENY`.
 Setting this option to `ALLOW` will cause the `X-Frame-Options` header to be omitted
 the the page can be displayed in a frame.
@@ -2644,17 +2693,17 @@
 Allow Gerrit to start even if the underlying schema version has been bumped to
 the next Gerrit version.
 +
-Set to true if Gerrit is installed in
+Set to `true` if Gerrit is installed in
 [high-availability configuration](https://gerrit.googlesource.com/plugins/high-availability/+/refs/heads/master/README.md)
 during the rolling upgrade to the next version.
 +
-By default false.
+By default `false`.
 +
 The rolling upgrade process, at high level, assumes that Gerrit is installed
 on two or more nodes sharing the repositories over NFS. The upgrade is composed
 of the following steps:
 +
-1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
+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 (where applicable)
@@ -2672,7 +2721,7 @@
 ServerId of the repositories imported from other Gerrit servers. Changes coming
 associated with the imported serverIds are indexed and displayed in the UI
 but they are not searchable by `changeNumber` therefore the
-`index.cacheQueryResultsByChangeNum` must also be set to false.
+`index.cacheQueryResultsByChangeNum` must also be set to `false`.
 Imported changes are still discoverable in any other ways, for example:
 
   project:someproject branch:main changeId:I78a7add1fe2597cad788c833d8f771f09b54cf33
@@ -2844,7 +2893,7 @@
 [[groups.auditLog.ignoreRecordsFromUnidentifiedUsers]]groups.auditLog.ignoreRecordsFromUnidentifiedUsers::
 +
 Controls whether AuditLogReader should ignore commits created by unidentified users.
-If true, then AuditLogReader ignores commits in the refs/groups/* made by unidentified users (i.e.
+If `true`, then AuditLogReader ignores commits in the refs/groups/* made by unidentified users (i.e.
 when the author of a commit can't be parsed as account id).
 +
 The current version of Gerrit writes identified users as authors for new refs/groups/* commits.
@@ -2852,9 +2901,9 @@
 <server@googlesource.com>") for such commits. Such string can't be converted to account id but
 usually the commit shouldn't be ignored.
 +
-By default, false.
+By default, `false`.
 +
-Setting it to true may lead to some unexpected results in audit log and must be set carefully.
+Setting it to `true` may lead to some unexpected results in audit log and must be set carefully.
 
 [[groups.includeExternalUsersInRegisteredUsersGroup]]groups.includeExternalUsersInRegisteredUsersGroup::
 +
@@ -2866,14 +2915,14 @@
 or a custom authentication backend). By default, Gerrit core always requires
 users to register and doesn't use external users.
 +
-By default, true.
+By default, `true`.
 
 [[groups.newGroupsVisibleToAll]]groups.newGroupsVisibleToAll::
 +
 Controls whether newly created groups should be by default visible to
 all registered users.
 +
-By default, false.
+By default, `false`.
 
 [[groups.uuid.name]]groups.<uuid>.name::
 +
@@ -2988,7 +3037,7 @@
 
 [[http.addUserAsRequestAttribute]]http.addUserAsRequestAttribute::
 +
-If true, 'User' attribute will be added to the request attributes so it
+If `true`, 'User' attribute will be added to the request attributes so it
 can be accessed outside the request scope (will be set to username or id
 if username not configured).
 +
@@ -3003,15 +3052,15 @@
 Pattern to print user in Tomcat AccessLog.
 
 +
-Default value is true.
+Default value is `true`.
 
 [[http.addUserAsResponseHeader]]http.addUserAsResponseHeader::
 +
-If true, the header 'User' will be added to the list of response headers so it
+If `true`, the header 'User' will be added to the list of response headers so it
 can be accessed from a reverse proxy for logging purposes.
 
 +
-Default value is false.
+Default value is `false`.
 
 [[httpd]]
 === Section httpd
@@ -3140,12 +3189,12 @@
 
 [[httpd.reuseAddress]]httpd.reuseAddress::
 +
-If true, permits the daemon to bind to the port even if the port
-is already in use.  If false, the daemon ensures the port is not
+If `true`, permits the daemon to bind to the port even if the port
+is already in use.  If `false`, the daemon ensures the port is not
 in use before starting.  Busy sites may need to set this to true
 to permit fast restarts.
 +
-By default, true.
+By default, `true`.
 
 [[httpd.gracefulStopTimeout]]httpd.gracefulStopTimeout::
 +
@@ -3164,11 +3213,11 @@
 
 [[httpd.inheritChannel]]httpd.inheritChannel::
 +
-If true, permits the daemon to inherit its server socket channel
-from fd0/1(stdin/stdout). When set to true, the server can be socket
+If `true`, permits the daemon to inherit its server socket channel
+from fd0/1(stdin/stdout). When set to `true`, the server can be socket
 activated via systemd or xinetd.
 +
-By default, false.
+By default, `false`.
 
 [[httpd.requestHeaderSize]]httpd.requestHeaderSize::
 +
@@ -3239,8 +3288,8 @@
 `log4j.appender` with the name `httpd_log` can be configured to overwrite
 programmatic configuration.
 +
-By default, true if httpd.listenUrl uses http:// or https://,
-and false if httpd.listenUrl uses proxy-http:// or proxy-https://.
+By default, `true` if httpd.listenUrl uses http:// or https://,
+and `false` if httpd.listenUrl uses proxy-http:// or proxy-https://.
 
 [[httpd.acceptorThreads]]httpd.acceptorThreads::
 +
@@ -3392,7 +3441,7 @@
 +
 Enable (or disable) registration of Jetty MBeans for Java JMX.
 +
-By default, false.
+By default, `false`.
 
 [[index]]
 === Section index
@@ -3436,7 +3485,7 @@
 It needs to be turned off when having Changes imported from other servers
 because of the potential conflicts of change numbers.
 +
-Defaults to true.
+Defaults to `true`.
 
 [[index.onlineUpgrade]]index.onlineUpgrade::
 +
@@ -3445,10 +3494,10 @@
 Gerrit version upgrades (avoiding the need for an offline reindex step
 using Reindex), but can add additional server load during the upgrade.
 +
-If set to false, there is no way to upgrade the index schema to take
+If set to `false`, there is no way to upgrade the index schema to take
 advantage of new search features without restarting the server.
 +
-Defaults to true.
+Defaults to `true`.
 
 [[index.excludeProjectFromChangeReindex]]index.excludeProjectFromChangeReindex::
 +
@@ -3459,6 +3508,21 @@
 Excluded projects can later be reindexed by for example using the
 link:cmd-index-changes-in-project.html[index changes in project command].
 
+[[index.reuseExistingDocuments]]index.reuseExistingDocuments::
++
+Whether to reuse index documents that already exist during reindexing.
++
+Currently, only supported by the changes index.
++
+This feature is useful, if the Gerrit server has to be restarted
+during an ongoing index online upgrade, since this would cause
+a complete reindexing otherwise that might take an extensive time.
++
+Each existing document in the index will be checked for staleness
+and reindexed if found to be stale.
++
+Defaults to false.
+
 [[index.paginationType]]index.paginationType::
 +
 The pagination type to use when index queries are repeated to
@@ -3576,11 +3640,11 @@
 [[index.autoReindexIfStale]]index.autoReindexIfStale::
 +
 Whether to automatically check if a document became stale in the index
-immediately after indexing it. If false, there is a race condition during two
+immediately after indexing it. If `false`, there is a race condition during two
 simultaneous writes that may cause one of the writes to not be reflected in the
 index. The check to avoid this does consume some resources.
 +
-Defaults to false.
+Defaults to `false`.
 
 [[index.indexChangesAsync]]index.indexChangesAsync::
 +
@@ -3590,7 +3654,7 @@
 to the write request latency) and disadvantage that the indexing result might not be
 immediately available after the write request.
 +
-Defaults to false.
+Defaults to `false`.
 
 [[index.scheduledIndexer]]
 ==== Subsection index.scheduledIndexer
@@ -3732,7 +3796,7 @@
 link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#enableAutoIOThrottle()[
 Lucene documentation,role=external,window=_blank] for further details.
 +
-Defaults to true (throttling enabled).
+Defaults to `true` (throttling enabled).
 
 During offline reindexing, setting ramBufferSize greater than the size
 of index (size of specific index folder under <site_dir>/index) and
@@ -3794,7 +3858,7 @@
 specific content, e.g.: `recheck`. Jenkins Gerrit Trigger plugin and Zuul CI
 depend on this feature to trigger change verification.
 +
-By default, true.
+By default, `true`.
 
 [[event.stream-events.enableRefUpdatedEvents]]event.stream-events.enableRefUpdatedEvents::
 +
@@ -3804,7 +3868,7 @@
 Please consider switching to `batch-ref-updated` event which provides better control on grouping and
 preserving order of the ref updates.
 +
-By default, true.
+By default, `true`.
 
 [[event.stream-events.enableBatchRefUpdatedEvents]]event.stream-events.enableBatchRefUpdatedEvents::
 +
@@ -3812,9 +3876,9 @@
 refs updated during a single batch ref update operation.
 Single ref updates are also streamed as a `batch-ref-updated` events with a single ref specified.
 This allows event listeners to react on all ref updated events and disable individual `ref-updated`
-events by setting <<event.stream-events.enableRefUpdatedEvents, event.stream-events.enableRefUpdatedEvents>> to false.
+events by setting <<event.stream-events.enableRefUpdatedEvents, event.stream-events.enableRefUpdatedEvents>> to `false`.
 +
-By default, false.
+By default, `false`.
 
 [[event.stream-events.enableDraftCommentEvents]]event.stream-events.enableDraftCommentEvents::
 +
@@ -3825,7 +3889,7 @@
 The extra amount of events depends on the usage pattern of the installation. It is worth evaluating
 the amount of extra events produced before enabling this flag by counting the calls to the draft APIs.
 +
-By default, false.
+By default, `false`.
 
 [[experiments]]
 === Section experiments
@@ -3898,7 +3962,7 @@
 temporarily inaccessible by users even with LDAP membership and grants
 referenced in the ACLs.
 +
-By default, true.
+By default, `true`.
 
 [[ldap.server]]ldap.server::
 +
@@ -3917,31 +3981,31 @@
 
 [[ldap.startTls]]ldap.startTls::
 +
-If true, Gerrit will perform StartTLS extended operation.
+If `true`, Gerrit will perform StartTLS extended operation.
 +
-By default, false, StartTLS will not be enabled.
+By default, `false`, StartTLS will not be enabled.
 
 [[ldap.supportAnonymous]]ldap.supportAnonymous::
 +
-If false, Gerrit will provide credentials only at connection open, this is
+If `false`, Gerrit will provide credentials only at connection open, this is
 required for some `LDAP` implementations that do not allow anonymous bind
 for StartTLS or for reauthentication.
 +
-By default, true.
+By default, `true`.
 
 [[ldap.sslVerify]]ldap.sslVerify::
 +
-If false and ldap.server is an `ldaps://` style URL or `ldap.startTls`
-is true, Gerrit will not verify the server certificate when it connects
+If `false` and ldap.server is an `ldaps://` style URL or `ldap.startTls`
+is `true`, Gerrit will not verify the server certificate when it connects
 to perform a query.
 +
-By default, true, requiring the certificate to be verified.
+By default, `true`, requiring the certificate to be verified.
 
 [[ldap.groupsVisibleToAll]]ldap.groupsVisibleToAll::
 +
-If true, LDAP groups are visible to all registered users.
+If `true`, LDAP groups are visible to all registered users.
 +
-By default, false, LDAP groups are visible only to administrators and
+By default, `false`, LDAP groups are visible only to administrators and
 group members.
 
 [[ldap.username]]ldap.username::
@@ -4178,7 +4242,7 @@
 +
 Converts the local username, that is used to login into the Gerrit
 Web UI, to lower case before doing the LDAP authentication. By setting
-this parameter to true, a case insensitive login to the Gerrit Web UI
+this parameter to `true`, a case insensitive login to the Gerrit Web UI
 can be achieved.
 +
 If set, it must be ensured that the local usernames for all existing
@@ -4191,7 +4255,7 @@
 case can't be undone. For newly created accounts the local username
 will be directly stored in lower case.
 +
-By default, unset/false.
+By default, unset/`false`.
 
 [[ldap.authentication]]ldap.authentication::
 +
@@ -4210,7 +4274,7 @@
             required
             useTicketCache=true
             doNotPrompt=true
-            renewTGT=true;
+            renewTGT=`true`;
 };
 ----
 
@@ -4229,7 +4293,7 @@
 +
 _(Optional)_ Enable the LDAP connection pooling or not.
 +
-If it is true, the LDAP service provider maintains a pool of (possibly)
+If it is `true`, the LDAP service provider maintains a pool of (possibly)
 previously used connections and assigns them to a Context instance as
 needed. When a Context instance is done with a connection (closed or
 garbage collected), the connection is returned to the pool for future use.
@@ -4238,7 +4302,7 @@
 LDAP connection management (Pool),role=external,window=_blank] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[
 LDAP connection management (Configuration),role=external,window=_blank]
 +
-By default, false.
+By default, `false`.
 
 [[ldap.connectTimeout]]ldap.connectTimeout::
 +
@@ -4284,7 +4348,7 @@
 
 [[log.jsonLogging]]log.jsonLogging::
 +
-If set to true, enables error, ssh and http logging in JSON format (file names:
+If set to `true`, enables error, ssh and http logging in JSON format (file names:
 `logs/error_log.json`, `logs/sshd_log.json` and `logs/httpd_log.json`).
 +
 The option only applies to Gerrit built-in loggers. It is ignored when a log4j
@@ -4292,11 +4356,11 @@
 link:#container.javaOptions[container.javaOptions], for example
 `-Dlog4j.configuration=file://etc/log4j.properties`.
 +
-Defaults to false.
+Defaults to `false`.
 
 [[log.textLogging]]log.textLogging::
 +
-If set to true, enables error logging in regular plain text format. Can only be disabled
+If set to `true`, enables error logging in regular plain text format. Can only be disabled
 if `jsonLogging` is enabled.
 +
 The option only applies to Gerrit built-in loggers. It is ignored when a log4j
@@ -4304,20 +4368,47 @@
 link:#container.javaOptions[container.javaOptions], for example
 `-Dlog4j.configuration=file://etc/log4j.properties`.
 +
-Defaults to true.
+Defaults to `true`.
 
 [[log.compress]]log.compress::
 +
-If set to true, log files are compressed at server startup and then daily at 11pm
+If set to `true`, log files are compressed at server startup and then daily at 11pm
 (in the server's local time zone).
 +
-Defaults to true.
+Defaults to `true`.
 
 [[log.rotate]]log.rotate::
 +
-If set to true, log files are rotated daily at midnight (GMT).
+If set to `true`, log files are rotated daily at midnight (GMT).
 +
-Defaults to true.
+Defaults to `true`.
+
+[[log.daysToKeep]]log.timeToKeep::
++
+Time that logs should be kept until they are being deleted. Values should use common
+suffixes to express their setting:
++
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
++
+The minimum granularity is days. Using a smaller time unit will result in deletion of
+all old logs, as if `0d` would have been configured.
++
+Actively used logs will never be deleted. Thus, this feature only works in combination
+with enabled link:#log.rotate[log.rotate]. Log deletion happens at server startup and
+then daily at 11pm (in the server's local time zone).
++
+Depending on the filesystem the following file times will be used, in order of priority:
++
+* Time of file creation
+* Time when the file was last modified
+* Date added to the filename as part of log file rotation. Time will be set to `00:00:00Z`.
++
+If none of the above is available, the log file won't be deleted.
++
+Defaults to `-1`, i.e. being disabled.
 
 [[metrics]]
 === Section metrics
@@ -4364,13 +4455,13 @@
 
 [[mimetype.name.safe]]mimetype.<name>.safe::
 +
-If set to true, files with the MIME type `<name>` will be sent as
+If set to `true`, files with the MIME type `<name>` will be sent as
 direct downloads to the user's browser, rather than being wrapped up
 inside of zipped archives.  The type name may be a complete type
 name, e.g. `image/gif`, a generic media type, e.g. `+image/*+`,
 or the wildcard `+*/*+` to match all types.
 +
-By default, false for all MIME types.
+By default, `false` for all MIME types.
 
 Common examples:
 ----
@@ -4435,16 +4526,16 @@
 
 [[oauth.allowEditFullName]]oauth.allowEditFullName::
 +
-If true, the full name can be edited in the contact information.
+If `true`, the full name can be edited in the contact information.
 +
-Default is false.
+Default is `false`.
 
 [[oauth.allowRegisterNewEmail]]oauth.allowRegisterNewEmail::
 +
-If true, additional email addresses can be registered in the contact
+If `true`, additional email addresses can be registered in the contact
 information.
 +
-Default is false.
+Default is `false`.
 
 [[operator-alias]]
 === Section operator alias
@@ -4490,7 +4581,7 @@
 
 [[pack.deltacompression]]pack.deltacompression::
 +
-If true, delta compression between objects is enabled.  This may
+If `true`, delta compression between objects is enabled.  This may
 result in a smaller overall transfer for the client, but requires
 more server memory and CPU time.
 +
@@ -4523,8 +4614,8 @@
 [[plugins.allowRemoteAdmin]]plugins.allowRemoteAdmin::
 +
 Enable remote installation, enable and disable of plugins over HTTP
-and SSH.  If set to true Administrators can install new plugins
-remotely, or disable existing plugins.  Defaults to false.
+and SSH.  If set to `true` Administrators can install new plugins
+remotely, or disable existing plugins.  Defaults to `false`.
 
 [[plugins.mandatory]]plugins.mandatory::
 +
@@ -4550,6 +4641,37 @@
 This config can be used when gerrit migrates from a deprecated plugin to the new one. The new plugin
 can (temporary) accept push options of the old plugin without registering such options.
 
+[[plugins.loadPriority]]plugins.loadPriority::
++
+List of `pluginName`s required to have a specific loading order during Gerrit startup.
++
+Each entry should contain a plugin name defined in the `MANIFEST.MF` under
+`Gerrit-PluginName` or a plugin JAR file name. During the Gerrit startup
+the `loadPriority` will influence the loading sequence of the JAR plugins.
++
+NOTE: Non-JAR plugins (e.g. Scripting, PolyGerrit plugins, ApiModule) are not
+influenced by this setting.
++
+Gerrit will always load plugins defining `Gerrit-ApiModule` in their
+`MANIFEST.MF` first. Then will load other plugins according to:
+
+ * the order of `plugins.loadPriority` in `gerrit.config`,
+ * the natural order of plugin JAR file names in `plugins/` directory.
++
+Example:
+Assuming we have three plugins: `a-plugin.jar`, `b-plugin.jar`, `c-plugin.jar`
+deployed to `plugins/` directory. By default Gerrit will load them in the same order
+as they are listed above, as that follows the _natural storing order_. Now assume
+the below configuration
+
+----
+[plugins]
+  loadPriority = c-plugin
+  loadPriority = a-plugin
+----
+
+Gerrit will load `c-plugin` first, followed-up by `a-plugin` and `b-plugin` last.
+
 [[receive]]
 === Section receive
 
@@ -4590,34 +4712,34 @@
 
 [[receive.checkMagicRefs]]receive.checkMagicRefs::
 +
-If true, Gerrit will verify the destination repository has
+If `true`, Gerrit will verify the destination repository has
 no references under the magic 'refs/for' branch namespace. Names under
 these locations confuse clients when trying to upload code reviews so
 Gerrit requires them to be empty.
 +
-If false Gerrit skips the sanity check and assumes administrators
+If `false` Gerrit skips the sanity check and assumes administrators
 have ensured the repository does not contain any magic references.
-Setting to false to skip the check can decrease latency during push.
+Setting to `false` to skip the check can decrease latency during push.
 +
-Default is true.
+Default is `true`.
 
 [[receive.allowProjectOwnersToChangeParent]]receive.allowProjectOwnersToChangeParent::
 +
-If true, Gerrit will allow project owners to change the parent of a project.
+If `true`, Gerrit will allow project owners to change the parent of a project.
 +
 By default only Gerrit administrators are allowed to change the parent
 of a project. By allowing project owners to change parents, it may
 allow the owner to circumvent certain enforced rules (like important
 BLOCK rules).
 +
-Default is false.
+Default is `false`.
 +
 This value supports configuration reloads:
 link:cmd-reload-config.html[reload-config]
 
 [[receive.checkReferencedObjectsAreReachable]]receive.checkReferencedObjectsAreReachable::
 +
-If set to true, Gerrit will validate that all referenced objects that
+If set to `true`, Gerrit will validate that all referenced objects that
 are not included in the received pack are reachable by the user.
 +
 Carrying out this check on gits with many refs and commits can be a
@@ -4627,11 +4749,11 @@
 Only disable this check if you trust the clients not to forge SHA-1
 references to access commits intended to be hidden from the user.
 +
-Default is true.
+Default is `true`.
 
 [[receive.enableInMemoryRefCache]]receive.enableInMemoryRefCache::
 +
-If true, Gerrit will cache all refs advertised during push in memory and
+If `true`, Gerrit will cache all refs advertised during push in memory and
 base later receive operations on that cache.
 +
 Turning this cache off is considered experimental.
@@ -4640,17 +4762,17 @@
 offer an inverse lookup of object ID to ref name. When RefTable is used,
 this cache can be turned off (experimental) to get speed improvements.
 +
-Default is true.
+Default is `true`.
 
 [[receive.enableSignedPush]]receive.enableSignedPush::
 +
-If true, server-side signed push validation is enabled.
+If `true`, server-side signed push validation is enabled.
 +
 When a client pushes with `git push --signed`, this ensures that the
 push certificate is valid and signed with a valid public key stored in
 the `refs/meta/gpg-keys` branch of `All-Users`.
 +
-Defaults to false.
+Defaults to `false`.
 
 [[receive.maxBatchChanges]]receive.maxBatchChanges::
 +
@@ -4701,7 +4823,7 @@
 value is inherited from the parent project. When `true`, the value is
 inherited, otherwise it is not inherited.
 +
-Default is false, the value is not inherited.
+Default is `false`, the value is not inherited.
 
 [[receive.maxTrustDepth]]receive.maxTrustDepth::
 +
@@ -4900,18 +5022,18 @@
 Whether Gerrit should automatically retry operations on failure with tracing
 enabled. The automatically generated traces can help with debugging.
 +
-By default this is set to false.
+By default this is set to `false`.
 
 [[rules]]
 === Section rules
 
 [[rules.enable]]rules.enable::
 +
-If true, Gerrit will load and execute 'rules.pl' files in each
-project's refs/meta/config branch, if present. When set to false,
+If `true`, Gerrit will load and execute 'rules.pl' files in each
+project's refs/meta/config branch, if present. When set to `false`,
 only the default internal rules will be used.
 +
-Default is true, to execute project specific rules.
+Default is `true`, to execute project specific rules.
 
 [[rules.reductionLimit]]rules.reductionLimit::
 +
@@ -4944,7 +5066,7 @@
 source files may need a larger rules.compileReductionLimit.  Consider
 using link:pgm-rulec.html[rulec] to precompile larger rule files.
 +
-A size of 0 bytes disables rules, same as rules.enable = false.
+A size of 0 bytes disables rules, same as rules.enable = `false`.
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 +
@@ -5038,7 +5160,7 @@
 email is delivered to the inbox. In this case, Gerrit will process the email
 immediately and will not have a fetch delay.
 +
-Defaults to false.
+Defaults to `false`.
 
 [[receiveemail.filter.mode]]receiveemail.filter.mode::
 +
@@ -5072,18 +5194,18 @@
 
 [[sendemail.enable]]sendemail.enable::
 +
-If false Gerrit will not send email messages, for any reason,
+If `false` Gerrit will not send email messages, for any reason,
 and all other properties of section sendemail are ignored.
 +
-By default, true, allowing notifications to be sent.
+By default, `true`, allowing notifications to be sent.
 
 [[sendemail.html]]sendemail.html::
 +
-If false, Gerrit will only send plain-text emails.
-If true, Gerrit will send multi-part emails with an HTML and
+If `false`, Gerrit will only send plain-text emails.
+If `true`, Gerrit will send multi-part emails with an HTML and
 plain text part.
 +
-By default, true, allowing HTML in the emails Gerrit sends.
+By default, `true`, allowing HTML in the emails Gerrit sends.
 
 [[sendemail.connectTimeout]]sendemail.connectTimeout::
 +
@@ -5175,11 +5297,11 @@
 
 [[sendemail.sslVerify]]sendemail.sslVerify::
 +
-If false and sendemail.smtpEncryption is 'ssl' or 'tls', Gerrit
+If `false` and sendemail.smtpEncryption is 'ssl' or 'tls', Gerrit
 will not verify the server certificate when it connects to send
 an email message.
 +
-By default, true, requiring the certificate to be verified.
+By default, `true`, requiring the certificate to be verified.
 
 [[sendemail.smtpUser]]sendemail.smtpUser::
 +
@@ -5216,12 +5338,12 @@
 
 [[sendemail.includeDiff]]sendemail.includeDiff::
 +
-If true, new change emails and merged change emails from Gerrit
+If `true`, new change emails and merged change emails from Gerrit
 will include the complete unified diff of the change.
 Variable maxmimumDiffSize places an upper limit on how large the
 email can get when this option is enabled.
 +
-By default, false.
+By default, `false`.
 
 [[sendemail.maximumDiffSize]]sendemail.maximumDiffSize::
 +
@@ -5271,12 +5393,12 @@
 
 [[sendemail.addInstanceNameInSubject]]sendemail.addInstanceNameInSubject::
 +
-When set to true, Gerrit will add its short name to the email subject, allowing recipients to quickly identify
+When set to `true`, Gerrit will add its short name to the email subject, allowing recipients to quickly identify
 what Gerrit instance the email came from.
 +
 The short name can be customized via the gerrit.instanceName option.
 +
-Defaults to false.
+Defaults to `false`.
 
 
 [[site]]
@@ -5296,9 +5418,9 @@
 
 [[site.refreshHeaderFooter]]site.refreshHeaderFooter::
 +
-If true the server checks the site header, footer and CSS files for
-updated versions. If false, a server restart is required to change
-any of these resources. Default is true, allowing automatic reloads.
+If `true` the server checks the site header, footer and CSS files for
+updated versions. If `false`, a server restart is required to change
+any of these resources. Default is `true`, allowing automatic reloads.
 
 [[ssh-alias]]
 === Section ssh-alias
@@ -5384,7 +5506,7 @@
 
 [[sshd.tcpKeepAlive]]sshd.tcpKeepAlive::
 +
-If true, enables TCP keepalive messages to the other side, so
+If `true`, enables TCP keepalive messages to the other side, so
 the daemon can terminate connections if the peer disappears.
 +
 Only effective when `sshd.backend` is set to `MINA`.
@@ -5721,7 +5843,7 @@
 If link:access-control.html#service_users[service users] should be skipped when
 suggesting reviewers.
 +
-By default true.
+By default `true`.
 
 [[tracing]]
 === Section tracing
@@ -5741,7 +5863,7 @@
 heap increase wasn't nearly as dramatic and the impact is most likely
 dependent on which plugin is used.
 +
-By default, false.
+By default, `false`.
 
 [[tracing.traceid]]
 ==== Subsection tracing.<trace-id>
@@ -6033,7 +6155,7 @@
 
 Note that the task will only be scheduled if the
 link:#autoUpdateAccountActiveStatus[auth.autoUpdateAccountActiveStatus]
-is set to true.
+is set to `true`.
 
 link:#schedule-configuration-examples[Schedule examples] can be found
 in the link:#schedule-configuration[Schedule Configuration] section.
@@ -6059,7 +6181,7 @@
 +
 This allows to enable the superproject subscription mechanism.
 +
-By default this is true.
+By default this is `true`.
 
 [[submodule.maxCombinedCommitMessageSize]]submodule.maxCombinedCommitMessageSize::
 +
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ce63295..29d1b85 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -24,7 +24,7 @@
 informed about this by a message in the git output. In addition,
 outdated votes are also listed in the email notification that is sent
 for the new patch set (unless this is disabled by a custom email
-template). Note, that the uploader only get this email notification if
+template). Note, that the uploader only gets this email notification if
 they have configured `Every Comment` for `Email notifications` in their
 user preferences. With any other email preference the email sender, the
 uploader in this case, is not included in the email recipients.
@@ -251,7 +251,7 @@
 change is allowed for submission. Label functions are **deprecated** and updates
 that set `function` to a blocking value {`MaxWithBlock`, `MaxNoBlock`,
 `AnyWithBlock`} will be rejected. Existing label function definitions can only
-be updated to {`NoBlock`, `NoOp`, `PatchSetLock`}. New label defintions should
+be updated to {`NoBlock`, `NoOp`, `PatchSetLock`}. New label definitions should
 also explicitly set the `function` attribute to a non-blocking value since the
 default is `MaxWithBlock`.
 
@@ -269,6 +269,7 @@
 
 Valid values are:
 
+[[MaxWithBlock]]
 * `MaxWithBlock` (default)
 +
 The lowest possible negative value, if present, blocks a submit, while
@@ -276,22 +277,26 @@
 must be at least one positive value, or else submit will never be
 enabled. To permit blocking submits, ensure a negative value is defined.
 
+[[AnyWithBlock]]
 * `AnyWithBlock`
 +
 The label is not mandatory but the lowest possible negative value,
 if present, blocks a submit. To permit blocking submits, ensure that a
 negative value is defined.
 
+[[MaxNoBlock]]
 * `MaxNoBlock`
 +
 The highest possible positive value is required to enable submit, but
 the lowest possible negative value will not block the change.
 
+[[NoBlock]]
 * `NoBlock`/`NoOp`
 +
 The label is purely informational and values are not considered when
 determining whether a change is submittable.
 
+[[PatchSetLock]]
 * `PatchSetLock`
 +
 The `PatchSetLock` function provides a locking mechanism for patch
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 187cd0f..81f9d9f 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -1,95 +1,496 @@
-= Gerrit Code Review - Project Configuration File Format
+= Gerrit Code Review - Project Configuration
 
-This page explains the storage format of Gerrit's project configuration
-and access control models.
+Every project has a configuration that defines access rights and controls
+project-specific behavior.
 
-The web UI access control panel is a front end for human-readable
-configuration files under the +refs/meta/config+ namespace in the
-affected project.  Direct manipulation of these files is mainly
-relevant in an automation scenario of the access controls.
-
+The project configuration is stored inside the git repository inside the
+`refs/meta/config` branch.
 
 [[refs-meta-config]]
-== The +refs/meta/config+ namespace
+== The `refs/meta/config` branch
 
-The namespace contains three different files that play different
-roles in the permission model.  With read permission to that reference,
-it is possible to fetch the +refs/meta/config+ reference to a local
-repository.  A nice side effect is that you can also upload changes
-to project permissions and review them just like with regular code
-changes. The preview changes option is also provided on the UI. Please note
-that you will have to configure push rights for the +refs/meta/config+ name
-space if you'd like to use the possibility to automate permission updates.
+The `refs/meta/config` branch contains configuration files. It is disconnected
+from the normal branches under `refs/heads/` that contain the source code.
 
-== Property inheritance
+The files inside the `refs/meta/config` branch are versioned just like any
+other file in the repository. This means from the git history of the
+`refs/meta/config` branch it can be seen how the configuration changed
+over time and which configuration was active when.
 
-If a property is set to INHERIT, then the value from the parent project is
-used. If the property is not set in any parent project, the default value is
-FALSE.
+[[configuration-files]]
+== Configuration files
+
+The project configuration is stored in the following files (these files are
+stored inside the `refs/meta/config` branch, see
+link:#refs-meta-config[above]):
+
+* link:#file-project_config[project.config]:
+  Contains the link to the parent project, the project description, access
+  rights, label definitions, submit requirements and options to control
+  project-specific behavior.
+* link:#file-groups[groups]:
+  Resolves group names that are mentioned in `project.config` to group UUIDs.
+* [DEPRECATED] link:#file-rules_pl[rules.pl]:
+  Contains Prolog rules that control when a change becomes ready to submit.
+  Note, Prolog rules are deprecated and have been replaced by
+  link:config-submit-requirements.html[submit requirements].
+
+In addition, there can be configuration files from Gerrit plugins, usually they
+are named `<PLUGIN-NAME>.config`.
+
+[[update]]
+== Updating the project configuration
+
+There are several possibilities to update the project configuration:
+
+[[update-through-web-ui]]
+* Through the Gerrit web UI:
++
+[[update-through-general-screen]]
+--
+* On the project's `General` screen:
++
+The `General` screen can be found under:
+`BROWSE` > `Repositories` > `<project-name>` > `General`
++
+In the `Configurations` section this screen allows to edit the project
+description and change repository options.
++
+Note, this screen only supports changing the most important repository options,
+but it doesn't expose all options that exist.
++
+All changes are directly applied (without code review).
+--
++
+[[update-through-access-screen]]
+--
+* On the project's `Access` screen:
++
+The `Access` screen can be found under:
+`BROWSE` > `Repositories` > `<project-name>` > `Access`
++
+This screen allows to edit the project's access rights and the parent project.
++
+Updating access rights via the `Access` screen updates the
+link:#file-groups[groups] file automatically.
++
+Modifications can either be applied directly (`SAVE` button) or saved for
+review (`SAVE FOR REVIEW` button).
++
+Saving access modification for review (`SAVE FOR REVIEW` button) creates an
+open change for the `refs/meta/config` branch where the modifications can be
+reviewed by a project owner. The modifications become effective only after a
+project owner submits the change.
+--
++
+[[update-via-online-editing]]
+--
+** Via online editing:
++
+The project's `Commands` screen, that can be found under `BROWSE` >
+`Repositories` > `<project-name>` > `Commands`, offers an `EDIT REPO CONFIG`
+command that allows to edit the `project.config` file directly in the Gerrit
+web UI.
++
+The `EDIT REPO CONFIG` command creates a new work-in-progress change with a
+change edit that contains the `project.config` file. Clicking on the command
+opens an online editor for the `project.config` file, allowing the user to make
+modifications to it.
++
+Modifications need to be saved and published ("saving" saves the modifications
+in the change edit, "publishing" publishes the change edit to make it visible
+to other users). While the change edit is not published yet, it's possible to
+add further files to it (e.g. the link:#file-groups[groups] file if it needs to
+be modified, in contrast to making access rights changes through the `Access`
+screen the `groups` file is not automatically updated). For further details how
+to work with change edits see the link:user-inline-edit.html[inline edit user
+guide]. After publishing the change edit, the change should be set to ready by
+clicking on `START REVIEW` > `SEND AND START REVIEW`. At this time you also
+want to add a project owner as a reviewer so that they can review and approve
+the change.
+--
+
+[[update-by-git-push]]
+* By pushing updates via Git:
++
+Since the configuration files are stored in a git branch, it's possible to
+update them via normal git operations:
++
+--
+1. Clone the repository if you don't have it available yet. The clone command
+   can be found in the `Download` section of project's `General` screen
+   (`BROWSE` > `Repositories` > `<project-name>` > `General`).
+2. Fetch and checkout the `refs/meta/config branch`, e.g. by `git fetch origin
+   refs/meta/config && git checkout FETCH_HEAD`.
+3. Edit the `project.config` file or other configuration files and commit the
+   changes, e.g. by `git commit --all`. Note, since the `project.config` file
+   uses the format of a git config file you can also edit it via the
+   `git config` command (e.g. to set a project description do: `git config -f
+   project.config project.description "My project description"`).
+4. Push the newly created commit, either to update the `refs/meta/config`
+   branch directly without code-review (e.g. `git push origin
+   HEAD:refs/meta/config`), or for review (e.g. `git push origin
+   HEAD:refs/for/refs/meta/config`).
+--
++
+[NOTE]
+Updates to access right may require changes in the link:#file-groups[groups]
+file. In contrast to making access rights changes through the `Access` screen
+the `groups` file is not automatically updated.
+
+[[update-through-rest-api]]
+* Through the Gerrit REST API:
++
+Gerrit offers several REST endpoints to modify the project configuration:
++
+** link:rest-api-projects.html#set-config[Set Config] REST endpoint:
+   Allows to edit the project description and change repository options.
+** link:rest-api-projects.html#set-project-description[Set Project Description]
+   REST endpoint:
+   Allows to set the project description.
+** link:rest-api-projects.html#delete-project-description[Delete Project
+   Description] REST endpoint:
+   Allows to unset the project description.
+** link:rest-api-projects.html#set-access[Add, Update and Delete Access Rights
+   for Project] REST endpoint:
+   Allows to edit the access rights of the project.
+** link:rest-api-projects.html#create-access-change[Create Access Rights Change
+   for review] REST endpoint:
+   Allows to create a change with access right modifications that can be
+   reviewed and submitted by a project owner.
+** link:rest-api-projects.html#create-label[Create Label] REST endpoint:
+   Allows to create a new label definition.
+** link:rest-api-projects.html#set-label[Set Label] REST endpoint:
+   Allows to update an existing label definition.
+** link:rest-api-projects.html#delete-label[Delete Label] REST endpoint:
+   Allows to delete an existing label definition.
+** link:rest-api-projects.html#create-submit-requirement[Create Submit
+   Requirement] REST endpoint:
+   Allows to create a new submit requirement.
+** link:rest-api-projects.html#update-submit-requirement[Update Submit
+   Requirement] REST endpoint:
+   Allows to update an existing submit requirement.
+** link:rest-api-projects.html#delete-submit-requirement[Delete Submit
+   Requirement] REST endpoint:
+   Allows to delete an existing submit requirement.
+
+[[required-permissions]]
+== Required permissions
+
+Depending on how the project configuration is changed different access rights
+are required:
+
+* Direct updates through the web UI (link:#update-through-general-screen[
+  updates through the `General` screen] and link:#update-through-access-screen[
+  direct updates of access rights through the `Access` screen via the `SAVE`
+  button]) and direct updates through the REST API (via the
+  link:rest-api-projects.html#set-config[Set Config] REST endpoint or the
+  link:rest-api-projects.html#set-access[Add, Update and Delete Access Rights
+  for Project] REST endpoint) require the user to be a project owner (have the
+  link:access-control.html#category_owner[Owner] access right assigned on
+  `refs/*` or have the link:access-control.html#capability_administrateServer[
+  Administrate Server] global capability assigned on the `All-Projects` root
+  project).
+* link:#update-by-git-push[Direct updates through `git push`] require the
+  user to have the link:access-control.html#category_push[Push] access right
+  assigned on `refs/meta/config` and be a project owner (have the
+  link:access-control.html#category_owner[Owner] access right assigned on
+  `refs/*` or have the link:access-control.html#capability_administrateServer[
+  Administrate Server] global capability assigned on the `All-Projects` root
+  project).
+* Creating changes for updates through the web UI
+  (link:#update-through-access-screen[proposing updates of access rights
+  through the `Access` screen via the `SAVE FOR REVIEW` button] and
+  link:#update-via-online-editing[proposing updates via the `EDIT REPO CONFIG`
+  command]), creating changes for updates through the REST API (via the
+  link:rest-api-projects.html#create-access-change[Create Access Rights Change
+  for review] REST endpoint) and link:#update-by-git-push[pushing changes for
+  review] require the user to be able to see the `refs/meta/config` branch
+  (have the link:access-control.html#category_read[Read] access right assigned
+  on `refs/meta/config`) and be allowed to create changes for it (have the
+  link:access-control.html#category_push[Push] access right assigned on
+  `refs/for/refs/meta/config` or be a project owner or be an administrator).
+
+[[comments]]
+=== Comments in project configuration files
+
+In principle it's possible to have comments in the project configuration files
+(lines starting with '#'), however if any Gerrit API is used that lead to
+modifications in the configuration files the comments may be dropped. This is
+because when Gerrit parses the configuration files and writes them back with
+updates, comments are not preserved.
+
+[TIP]
+When updating the project configuration use the commit message to record the
+reason for the settings so that this information is preserved in the git
+history. For example, this allows using `git blame` to inspect why
+configuration values have been set.
+
+[[inheritance]]
+== Inheritance
+
+Projects in Gerrit are organized hierarchically in a tree with the
+`All-Projects` project as the root project. The parent project is defined in
+the link:#access.inheritFrom[access section] of the `project.config` file.
+
+Projects inherit access rights and options from their parent project, but not
+all options are inheritable. See the description of the options in the
+link:#file-project_config[project.config] file to learn whether they are
+inherited or not.
+
+Options with boolean values support a special `INHERIT` value to make them
+inherit the value that is set in the parent project.
+
+Some settings can be enforced for child projects (or if set on the
+`All-Projects` root project for all projects), e.g. access right restrictions
+via link:access-control#block[BLOCK rules] or
+link:config-submit-requirements.html#submit_requirement_can_override_in_child_projects[
+non-overridable] submit requirements.
+
+[NOTE]
+The parent project for an existing project can be changed via the
+link:update-through-access-screen[Access] screen (by default this is only
+allowed for administrators).
+
+[NOTE]
+Project owners can be allowed to change the parent of projects that they own
+(see link:config-gerrit.html#receive.allowProjectOwnersToChangeParent[
+receive.allowProjectOwnersToChangeParent] setting which is `false` by default).
+In this case project owners may escape the settings that are enforced by their
+parent project by choosing a different parent project.
 
 [[file-project_config]]
-== The file +project.config+
+== The file `project.config`
 
-The +project.config+ file contains the link between groups and their
-permitted actions on reference patterns in this project and any projects
-that inherit its permissions.
+The `project.config` file contains the link to the parent project, the project
+description, access rights, link:config-labels.html[label definitions],
+link:config-submit-requirements.html[submit requirements] and options to
+control project-specific behavior.
 
-The format in this file corresponds to the Git config file format, so
-if you want to automate your permissions it is a good idea to use the
-+git config+ command when writing to the file. This way you know you
-don't accidentally break the format of the file.
+The format in this file corresponds to the Git config file format.
 
-Here follows a +git config+ command example:
+[TIP]
+--
+Since the format of the `project.config` file adheres to the Git config file
+format, utilizing the `git config` command when modifying the file is
+recommended. This way you can avoid breaking the format of the file
+accidentally.
+
+Here is an example of a `git config` command that updates the project
+description:
 
 ----
-$ git config -f project.config project.description "Rights inherited by all other projects"
+$ git config -f project.config project.description "Foo Bar"
 ----
+--
 
-Below you will find an example of the +project.config+ file format:
+This is an example of a `project.config` file:
 
 ----
 [project]
-       description = Rights inherited by all other projects
+  description = Collection of scripts for setting up foo bar.
 [access "refs/*"]
-       read = group Administrators
+  read = group Registered Users
 [access "refs/heads/*"]
-        label-Your-Label-Here = -1..+1 group Administrators
-[capability]
-       administrateServer = group Administrators
+  label-Code-Review = -2..+2 group Maintainers
+  label-Code-Review = -1..+1 group Registered Users
+  label-Your-Label = -1..+1 group Maintainers
 [receive]
-       requireContributorAgreement = false
-[label "Your-Label-Here"]
-        function = MaxWithBlock
-        value = -1 Your -1 Description
-        value =  0 Your No score Description
-        value = +1 Your +1 Description
+  requireChangeId = true
+[label "Your-Label"]
+  function = NoOp
+  value = -1 Your -1 Description
+  value =  0 Your No score Description
+  value = +1 Your +1 Description
+[submit-requirement "Your-Label"]
+  description = At least one maximum vote for label 'Your-Label' is required
+  applicableIf = -branch:refs/meta/config
+  submittableIf = label:Your-Label=MAX AND -label:Your-Label=MIN
 ----
 
-As you can see, there are several sections.
+The sections that can appear in the `project.config` file are explained below.
 
-The link:#project-section[+project+ section] appears once per project.
+[[access-section]]
+=== Access section
 
-The link:#access-subsection[+access+ section] appears once per reference pattern,
-such as `+refs/*+` or `+refs/heads/*+`. Only one access section per pattern is
-allowed.
+[[access.inheritFrom]]access.inheritFrom::
++
+Name of the parent project from which access rights are inherited.
++
+If not set, access rights are inherited from the `All-Projects` root project.
 
-The link:#receive-section[+receive+ section] appears once per project.
+[[access-subsection]]
+==== Access subsection
 
-The link:#submit-section[+submit+ section] appears once per project.
+`access` subsections define access rules for a ref or a ref namespace. The ref
+or ref namespace is specified as the subsection name and can be a concrete ref
+(e.g. `refs/heads/master`), a ref pattern (last path segment is '\*', e.g.
+`refs/heads/*`) or a regular expression (must start with '^', e.g.
+`^refs/heads/rel-.*`).
 
-The link:#capability-section[+capability+] section only appears once, and only
-in the +All-Projects+ repository.  It controls core features that are configured
-on a global level.
+[NOTE]
+For ref patterns '\*' can only appear as the last path segment. If a '*' is
+required in any other place the ref namespace must be specified as a regular
+expression (must start with '^', '\*' must follow what's being matched, e.g.
+".*" to match any string).
 
-The link:#label-section[+label+] section can appear multiple times. You can
-also redefine the text and behavior of the built in label types `Code-Review`
-and `Verified`.
+[NOTE]
+Only one access subsection per ref and per ref namespace is allowed.
 
-Optionally a +commentlink+ section can be added to define project-specific
-comment links. The +commentlink+ section has the same format as the
-link:config-gerrit.html#commentlink[+commentlink+ section in gerrit.config]
+The `access` subsections contain access rules that apply to the ref or ref
+namespace of the `access` subsections. The format of the access rules is: +
+`<accessCategoryId> = (block|deny)? <range>? group <group-name>`
+
+* `<accessCategoryId>`: ID of the link:access-control.html#access_categories[
+  access category] for which the access rule should be defined. The ID of the
+  access category is the name of the access category in lowerCamelCase (e.g.
+  `createTag`), except for label permissions where it is `label-<label-Name>`
+  (e.g. `label-Code-Review`).
+* `(block|deny)?`: `block` defines a link:access-control.html#block-rule[BLOCK]
+  rule, `deny` defines a link:access-control.html#deny-rule[DENY] rule, if
+  neither `block` or `deny` is specified an link:access-control.html#allow-rule[
+  ALLOW] rule is defined.
+* `<range>?`: Only set for label permission. The voting range in the format
+  `<min-vote>..<max-vote>` (e.g. `-1..+1`).
+* `group <group-name>`: The (local) name of the group to which the access rule
+  should apply (e.g. `group Foo Bar`). The (local) group name must exist in the
+  link:#file-groups[groups] file, so that Gerrit can resolve it to the group
+  UUID.
+
+To make access rules link:access-control.html#exclusive[exclusive] they need to
+be included into the value of the `exclusiveGroupPermissions` key: +
+`exclusiveGroupPermissions = <space-separated-list-of-access-category-ids>`
+
+.Example access subsections
+----
+  [access "refs/heads/*"]
+    create = group Administrators
+    delete = group Administrators
+    deleteChanges = group Administrators
+    label-Code-Review = -2..+2 group Maintainers
+    label-Code-Review = -1..+1 group Registered Users
+    label-Verified = -1..+1 group Registered Users
+    push = block Registered Users
+    submit = group Maintainers
+    exclusiveGroupPermissions = deleteChanges submit
+  [access "^refs/tags/rel-.*"]
+    createTag = group Maintainers
+    createSignedTag = group Maintainers
+----
+
+[[branchOrder-section]]
+=== branchOrder section
+
+Defines a branch ordering which is used for backporting of changes.
+Backporting will be offered for a change (in the Gerrit UI) for all
+more stable branches where the change can merge cleanly.
+
+[[branchOrder.branch]]branchOrder.branch::
+
+A branch name, typically multiple values will be defined. The order of branch
+names in this section defines the branch order. The topmost is considered to be
+the least stable branch (typically the master branch) and the last one the
+most stable (typically the last maintained release branch).
+
+.Example:
+----
+[branchOrder]
+  branch = master
+  branch = stable-2.9
+  branch = stable-2.8
+  branch = stable-2.7
+----
+
+The `branchOrder` section is inheritable. This is useful when multiple or all
+projects follow the same branch rules. A `branchOrder` section in a child
+project completely overrides any `branchOrder` section from a parent i.e. there
+is no merging of `branchOrder` sections. A present but empty `branchOrder`
+section removes all inherited branch order.
+
+Branches not listed in this section will not be included in the mergeability
+check. If the `branchOrder` section is not defined then the mergeability of a
+change into other branches will not be computed.
+
+[[capability-section]]
+=== Capability section
+
+The `capability` section only appears once, and only in the `project.config`
+file of the `All-Projects` root project. It controls Gerrit administration
+capabilities that are configured on a global level.
+
+.Example:
+----
+[capability]
+  administrateServer = group Administrators
+----
+
+[NOTE]
+The (local) group names for which capabilities are assigned must exist in the
+link:#file-groups[groups] file, so that Gerrit can resolve them to their group
+UUID.
+
+Please refer to the
+link:access-control.html#global_capabilities[Global Capabilities]
+documentation for a full list of available capabilities.
+
+[[change-section]]
+=== Change section
+
+The change section includes configuration for project-specific change settings:
+
+[[change.privateByDefault]]change.privateByDefault::
++
+Controls whether all new changes in the project are set as private by default.
++
+Note that a new change will be public if the `is_private` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `remove-private` link:user-upload.html#private[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
+[[change.workInProgressByDefault]]change.workInProgressByDefault::
++
+Controls whether all new changes in the project are set as WIP by default.
++
+Note that a new change will be ready if the `workInProgress` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `ready` link:user-upload.html#wip[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
+[[commentlink-section]]
+=== Commentlink section
+
+Optionally a `commentlink` section can be added to define project-specific
+comment links. The `commentlink` section has the same format as the
+link:config-gerrit.html#commentlink[commentlink] section in `gerrit.config`
 which is used to define global comment links.
 
+[[label-section]]
+=== Label section
+
+Please refer to link:config-labels.html#label_custom[Custom Labels] documentation.
+
+[[mimetype-section]]
+=== MIME Types section
+
+The +mimetype+ section may be configured to force the web code
+reviewer to return certain MIME types by file path. MIME types
+may be used to activate syntax highlighting.
+
+----
+[mimetype "text/x-c"]
+  path = *.pkt
+[mimetype "text/x-java"]
+  path = api/current.txt
+----
+
 [[project-section]]
 === Project section
 
@@ -281,36 +682,31 @@
 where a commit is already merged into a branch and you want to create
 a new open change for that commit on another branch.
 
-[[change-section]]
-=== Change section
+[[reviewer-section]]
+=== reviewer section
 
-The change section includes configuration for project-specific change settings:
+Defines config options to adjust a project's reviewer workflow such as enabling
+reviewers and CCs by email.
 
-[[change.privateByDefault]]change.privateByDefault::
+[[reviewer.enableByEmail]]reviewer.enableByEmail::
 +
-Controls whether all new changes in the project are set as private by default.
+A boolean indicating if reviewers and CCs that do not currently have a Gerrit
+account can be added to a change by providing their email address.
 +
-Note that a new change will be public if the `is_private` field in
-link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
-when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
-or the `remove-private` link:user-upload.html#private[PushOption] is used during
-the Git push.
+This setting only takes effect for changes that are readable by anonymous users.
 +
 Default is `INHERIT`, which means that this property is inherited from
-the parent project.
+the parent project. If the property is not set in any parent project, the
+default value is `FALSE`.
 
-[[change.workInProgressByDefault]]change.workInProgressByDefault::
+[[reviewer.skipAddingAuthorAndCommitterAsReviewers]]reviewer.skipAddingAuthorAndCommitterAsReviewers::
 +
-Controls whether all new changes in the project are set as WIP by default.
-+
-Note that a new change will be ready if the `workInProgress` field in
-link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
-when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
-or the `ready` link:user-upload.html#wip[PushOption] is used during
-the Git push.
+Whether to skip adding the Git commit author and committer as reviewers for
+a new change.
 +
 Default is `INHERIT`, which means that this property is inherited from
-the parent project.
+the parent project. If the property is not set in any parent project, the
+default value is `FALSE`.
 
 [[submit-section]]
 === Submit section
@@ -457,9 +853,9 @@
 unless merge commits are being submitted. For some people this is an advantage
 since they find the linear history easier to read.
 +
-NOTE: Rebasing merge commits is not supported. If a change with a merge commit
-is submitted the link:#merge_if_necessary[merge if necessary] submit action is
-applied.
+NOTE: Rebasing merge commits is done by rebasing their first parent commit,
+i.e. the first parent is updated to the new base while the second parent stays
+intact.
 
 [[rebase_always]]
 * 'rebase always':
@@ -476,9 +872,9 @@
 unless merge commits are being submitted. For some people this is an advantage
 since they find the linear history easier to read.
 +
-NOTE: Rebasing merge commits is not supported. If a change with a merge commit
-is submitted the link:#merge_if_necessary[merge if necessary] submit action is
-applied.
+NOTE: Rebasing merge commits is done by rebasing their first parent commit,
+i.e. the first parent is updated to the new base while the second parent stays
+intact.
 +
 When rebasing the patchset, Gerrit automatically appends onto the end of the
 commit message a short summary of the change's approvals, and a URL link back
@@ -634,132 +1030,32 @@
 is set to 'true' the merge would fail in such a case. An empty commit is still allowed as the
 initial commit on a branch.
 
-
-[[access-section]]
-=== Access section
-
-[[access.inheritFrom]]access.inheritFrom::
-+
-Name of the parent project from which access rights are inherited.
-+
-If not set, access rights are inherited from the `All-Projects` root project.
-
-[[access-subsection]]
-==== Access subsection
-
-+access+ subsections for references connect access rights to groups. Each group
-listed must exist in the link:#file-groups[+groups+ file].
-
-Please refer to the
-link:access-control.html#access_categories[Access Categories]
-documentation for a full list of available access rights.
-
-
-[[mimetype-section]]
-=== MIME Types section
-
-The +mimetype+ section may be configured to force the web code
-reviewer to return certain MIME types by file path. MIME types
-may be used to activate syntax highlighting.
-
-----
-[mimetype "text/x-c"]
-  path = *.pkt
-[mimetype "text/x-java"]
-  path = api/current.txt
-----
-
-
-[[capability-section]]
-=== Capability section
-
-The +capability+ section only appears once, and only in the +All-Projects+
-repository.  It controls Gerrit administration capabilities that are configured
-on a global level.
-
-Please refer to the
-link:access-control.html#global_capabilities[Global Capabilities]
-documentation for a full list of available capabilities.
-
-[[label-section]]
-=== Label section
-
-Please refer to link:config-labels.html#label_custom[Custom Labels] documentation.
-
 [[submit-requirement-section]]
 === Submit Requirement section
 
 Please refer to link:config-submit-requirements.html[Configuring Submit
 Requirements] documentation.
 
-[[branchOrder-section]]
-=== branchOrder section
-
-Defines a branch ordering which is used for backporting of changes.
-Backporting will be offered for a change (in the Gerrit UI) for all
-more stable branches where the change can merge cleanly.
-
-[[branchOrder.branch]]branchOrder.branch::
-+
-A branch name, typically multiple values will be defined. The order of branch
-names in this section defines the branch order. The topmost is considered to be
-the least stable branch (typically the master branch) and the last one the
-most stable (typically the last maintained release branch).
-+
-Example:
-+
-----
-[branchOrder]
-  branch = master
-  branch = stable-2.9
-  branch = stable-2.8
-  branch = stable-2.7
-----
-+
-The `branchOrder` section is inheritable. This is useful when multiple or all
-projects follow the same branch rules. A `branchOrder` section in a child
-project completely overrides any `branchOrder` section from a parent i.e. there
-is no merging of `branchOrder` sections. A present but empty `branchOrder`
-section removes all inherited branch order.
-+
-Branches not listed in this section will not be included in the mergeability
-check. If the `branchOrder` section is not defined then the mergeability of a
-change into other branches will not be done.
-
-[[reviewer-section]]
-=== reviewer section
-
-Defines config options to adjust a project's reviewer workflow such as enabling
-reviewers and CCs by email.
-
-[[reviewer.enableByEmail]]reviewer.enableByEmail::
-+
-A boolean indicating if reviewers and CCs that do not currently have a Gerrit
-account can be added to a change by providing their email address.
-+
-This setting only takes effect for changes that are readable by anonymous users.
-+
-Default is `INHERIT`, which means that this property is inherited from
-the parent project. If the property is not set in any parent project, the
-default value is `FALSE`.
-
-[[reviewer.skipAddingAuthorAndCommitterAsReviewers]]reviewer.skipAddingAuthorAndCommitterAsReviewers::
-+
-Whether to skip adding the Git commit author and committer as reviewers for
-a new change.
-+
-Default is `INHERIT`, which means that this property is inherited from
-the parent project. If the property is not set in any parent project, the
-default value is `FALSE`.
-
 [[file-groups]]
-== The file +groups+
+== The file `groups`
 
-Each group in this list is linked with its UUID so that renaming of
-groups is possible without having to rewrite every +groups+ file
-in every repository where it's used.
+The `groups` file resolves group names that are mentioned in
+link:#access-subsection[access subsections] of the link:#file-project_config[
+project.config] file to group UUIDs.
 
-This is what the default groups file for +All-Projects.git+ looks like:
+In the access subsections access rights are assigned to group names. These
+group names are local to the project configuration and do not need to match
+with the actual group names. To enable Gerrit to resolve the local group names
+they must be mapped to group UUIDs in the `groups` file.
+
+The access sections use local group names, rather than requiring the actual
+group names, to allow renaming groups in Gerrit without having to rewrite every
+`project.config` file using the group.
+
+The content of the `groups` file is a simple table of group UUID to group name,
+separated by a tab.
+
+This is how the default `groups` file for `All-Projects` project looks like:
 
 ----
 # UUID                                         Group Name
@@ -771,29 +1067,37 @@
 global:Registered-Users                        Registered Users
 ----
 
-This file can't be written to by the +git config+ command.
+Since the `groups` file has a custom format it can't be edited using the
+`git config` command.
 
-In order to reference a group in +project.config+, it must be listed in
-the +groups+ file.  When editing permissions through the web UI this
-file is maintained automatically, but when pushing updates to
-+refs/meta/config+ this must be dealt with by hand.  Gerrit will refuse
-+project.config+ files that refer to groups not listed in +groups+.
+Whenever access rights in the `project.config` file are assigned to new groups
+mapping entries for the new groups must be added to the `groups` file. The
+modifications to the `groups` file must be included in the same commit that
+updates the `project.config` file. Pushing updates to `project.config` files
+that refer to groups not listed in the `groups` file are rejected by Gerrit.
 
-The UUID of a group can be found on the General tab of the group's page
-in the web UI or via the +-v+ option to
-link:cmd-ls-groups.html[the +ls-groups+ SSH command].
+When link:#update-through-access-screen[editing access rights through the web
+UI] the `groups` file is automatically updated by Gerrit.
 
+The UUID of a group can be found on the group screen (`BROWSE` > `Groups` >
+`<group-name>` ). Alternatively the group can be looked up via the
+link:rest-api-groups.html#get-group[Get Group] REST endpoint (note that the
+`group-id` in the URL can be the group name). The group UUID is contained as
+`id` field in the return link:rest-api-groups.html#group-info[GroupInfo] JSON.
 
 [[file-rules_pl]]
-== The file +rules.pl+
+== The file `rules.pl`
 
-The +rules.pl+ files allows you to replace or amend the default Prolog
-rules that control e.g. what conditions need to be fulfilled for a
-change to be submittable.  This file content should be
-interpretable by the 'Prolog Cafe' interpreter.
+The `rules.pl` file allows to replace or amend the default Prolog rules that
+control what conditions need to be fulfilled for a change to be submittable.
+This file should be interpretable by the 'Prolog Cafe' interpreter.
 
-You can read more about the +rules.pl+ file and the prolog rules on
-link:prolog-cookbook.html[the Prolog cookbook page].
+You can read more about prolog rules on the link:prolog-cookbook.html[Prolog
+cookbook] page.
+
+[NOTE]
+Prolog rules are deprecated and have been replaced by
+link:config-submit-requirements.html[submit requirements].
 
 GERRIT
 ------
diff --git a/Documentation/config-robot-comments.txt b/Documentation/config-robot-comments.txt
index 04309e5..ef99d80 100644
--- a/Documentation/config-robot-comments.txt
+++ b/Documentation/config-robot-comments.txt
@@ -1,5 +1,9 @@
 = Gerrit Code Review - Robot Comments
 
+[NOTE]
+Robot Comments are deprecated in favour of link:pg-plugin-checks-api.html[Checks API] and human
+comments.
+
 Gerrit has special support for inline comments that are generated by
 automated third-party systems, so called "robot comments". For example
 robot comments can be used to represent the results of code analyzers.
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 1bcda63..5ab1add 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -1,32 +1,375 @@
 = Gerrit Code Review - Submit Requirements
 
-As part of the code review process, project owners need to configure rules that
-govern when changes become submittable. For example, an admin might want to
-prevent changes from being submittable until at least a “+2” vote on the
-“Code-Review” label is granted on a change. Admins can define submit
-requirements to enforce submittability rules for changes.
+Submit requirements are rules that define when a change can be submitted. This
+page describes how to configure them.
 
 [[configuring_submit_requirements]]
 == Configuring Submit Requirements
 
-Site administrators and project owners can define submit requirements in the
-link:config-project-config.html[project config]. A submit requirement has the
-following fields:
+Submit requirements are defined as link:#submit-requirement-subsection[
+submit-requirement] subsections in the
+link:config-project-config.html#file-project_config[project.config] file. The
+subsection name defines the name of the submit requirement.
 
+[NOTE]
+There are multiple options how to update `project.config` files, please refer
+to the link:config-project-config.html#update[project config documentation].
 
-[[submit_requirement_name]]
-=== submit-requirement.Name
+[TIP]
+When modifying submit requirements it's recommended to
+link:#test-submit-requirements[test] them before updating them in the project
+configuration.
 
-A name that uniquely identifies the submit requirement. Submit requirements
-can be overridden in child projects if they are defined with the same name in
-the child project. See the link:#inheritance[inheritance] section for more
-details.
+[WARNING]
+--
+When adding submit requirements think about whether they should apply to the
+link:config-project-config.html#refs-meta-config[refs/meta/config] branch
+(see the link:#submit_requirement_applicable_if[applicableIf] description on
+how to exempt the `refs/meta/config` branch from a submit requirement). Since
+submit requirements are stored as part of the project configuration in the
+`refs/meta/config` branch, changing them through code review requires to pass
+the submit requirements that apply to the `refs/meta/config` branch. Hence by
+misconfiguring submit requirements for the `refs/meta/config` branch you can
+make further updates to submit requirements through code review impossible.
+If this happens the submit requirements can be restored by a direct push to the
+`refs/meta/config` branch.
+
+[[restore-submit-requirements]]
+If direct pushes are disabled or not allowed project owners can directly update
+the submit requirements via the
+link:rest-api-projects.html#update-submit-requirement[Update Submit Requirement]
+REST endpoint.
+
+.Example:
+----
+  curl -X PUT --header "Content-Type: application/json" -d '{"name": "Foo-Review", "description": "At least one maximum vote for the Foo-Review label is required", "submittability_expression": "label:Foo-Review=MAX AND -label:Foo-Review=MIN", "applicability_expression": "-branch:refs/meta/config", "canOverrideInChildProjects": true}' "https://<HOST>/a/projects/My%2FProject/submit_requirements/Foo-Review"
+----
+
+Tip: Googlers should use `gob-curl` instead of `curl` so that authentication is
+handled automatically.
+--
+
+[[test-submit-requirements]]
+=== Testing Submit Requirements
+
+When modifying submit requirements it's recommended to test them before
+updating them in the project configuration.
+
+To test a submit requirement on a selected change
+link:rest-api-changes.html#change-id[project\~branch~changeId] use the
+link:rest-api-changes.html#check-submit-requirement[Check Submit Requirement]
+REST endpoint.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+    {
+      "name": "Code-Review",
+      "submittability_expression": "label:Code-Review=+2"
+    }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "name": "Code-Review",
+    "status": "SATISFIED",
+    "submittability_expression_result": {
+      "expression": "label:Code-Review=+2",
+      "fulfilled": true,
+      "passingAtoms": [
+        "label:Code-Review=+2"
+      ]
+    },
+    "is_legacy": false
+  }
+----
+
+Alternatively you can make a change that updates a submit requirement in the
+`project.config` file, upload it for review to the `refs/meta/config` branch
+and then load it from that change which is in review to test it against a
+change.
+
+Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement?sr-name=Code-Review&refs-config-change-id=myProject~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d HTTP/1.0
+----
+
+In the above example `myProject\~master~I8473b95934b5732ac55d26311a706c9c2bde9940`
+is the change against which the submit requirement is tested, `Code Review` is
+the name of the submit requirement that is tested and
+`myProject\~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d` is the
+change from which the `Code Review` submit requirement is loaded. Change
+`myProject~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d` must be
+a change that touches the `project.config` file in the `refs/meta/config`
+branch and the `project.config` file must contain a submit requirement with the
+name `Code-Review`.
+
+[[dashboard]]
+=== Show Submit Requirements on Dashboards
+
+Gerrit offers dashboards that provide an overview over a set of changes (e.g.
+user dashboards shows changes that are relevant to the user, change list
+dashboards show changes that match a change query). To understand the state of
+the changes knowing the status of their submit requirements is important, but
+submit requirements are manifold and dashboards have only limited screen space
+available, so showing all submit requirements in dashboards is hardly possible.
+This is why administrators must decide which are the most important submit
+requirements that should be shown on dashboards. They can configure these
+submit requirements in `gerrit.config` by setting the
+link:config-gerrit.html#dashboard[dashboard.submitRequirementColumns] option.
+
+[NOTE]
+In order to save screen space submit requirement names on dashboards are
+abbreviated, e.g. a submit requirement called `Foo-Bar` is shown as `FB`.
+
+[[inheritance]]
+== Inheritance
+
+Submit requirements are inherited from parent projects. Child projects may
+override an inherited submit requirement by defining a submit requirement with
+the same name, but only if overriding the submit requirement is allowed (see
+link:#submit_requirement_can_override_in_child_projects[
+canOverrideInChildProjects] field). Overriding an inherited submit requirement
+always overrides the complete submit requirement definition, overriding single
+fields only is not possible.
+
+[NOTE]
+To remove an inherited submit requirement in a child project, set both the
+link:#submit_requirement_applicable_if[applicableIf] expression and the
+link:#submit_requirement_submittable_if[submittableIf] expression to
+`is:false`.
+
+[NOTE]
+If overriding a submit requirement is disallowed in a parent project, submit
+requirements with the same name in child projects, that would otherwise
+override the inherited submit requirement, are ignored.
+
+[[labels]]
+== Labels and Submit Requirements
+
+link:config-labels.html[Labels] define voting categories for reviewers to score
+changes. Often a label is accompanied by a submit requirement to check the votes
+on the label, e.g. with a link:#submit_requirement_submittable_if[submittableIf]
+expression that checks that:
+
+* the label was approved: `label:My-Label=MAX`
+* the label has no veto: `-label:My-Label=MIN`
+* the label was not self-approved: `label:My-Label=MAX,user=non_uploader`
+* the label was approved by multiple users: `label:My-Label,count>1`
+
+Submit requirements that check votes for a single label often have the same
+name as the label, e.g.:
+
+----
+[label "Code-Review"]
+  function = NoBlock
+  value = -2 This shall not be submitted
+  value = -1 I would prefer this is not merged as is
+  value = 0 No score
+  value = +1 Looks good to me, but someone else must approve
+  value = +2 Looks good to me, approved
+  defaultValue = 0
+[submit-requirement "Code-Review"]
+  description = At least one maximum vote for label 'Code-Review' is required
+  submittableIf = label:Code-Review=MAX,user=non_uploader AND -label:Code-Review=MIN
+  canOverrideInChildProjects = true
+----
+
+[[trigger-votes]]
+=== Trigger Votes
+
+Trigger votes are votes on labels that are not associated with any submit
+requirement expressions, i.e. the submittability of changes doesn't depend on
+these votes.
+
+Voting on labels that have no impact on the submittability of changes usually
+serves the purpose to trigger processes, e.g. a vote on a `Presubmit-Ready`
+label can be a signal to run presubmit integration tests. Hence these votes are
+called `trigger votes`.
+
+Trigger votes are displayed in a separate section in the change page.
+
+[[deprecated]]
+== Deprecated ways to control when changes can be submitted
+
+Using submit requirements is the recommended way to control when changes can be
+submitted. However, historically there are other ways for this, which are still
+working, although they are deprecated:
+
+[[label-functions]]
+* Label functions:
++
+link:config-labels.html#label_custom[Label definitions] can contain a
+link:config-labels.html#label_function[function] that impacts the
+submittability of changes (link:config-labels.html#MaxWithBlock[MaxWithBlock],
+link:config-labels.html#AnyWithBlock[AnyWithBlock],
+link:config-labels.html#MaxNoBlock[MaxNoBlock]). These functions are deprecated
+and setting them is no longer allowed, however if they are (already) set for
+existing label definitions they are still respected. For new labels the
+function should be set to link:config-labels.html#NoBlock[NoBlock] and then
+submit requirements should be used to control when changes can be submitted
+(using `submittableIf = label:My-Label=MAX AND -label:My-Label=MIN` is
+equivalent to `MaxWithBlock`, using `submittableIf = -label:My-Label=MIN` is
+equivalent to `AnyWithBlock`, using `submittableIf = label:My-Label=MAX` is
+equivalent to using `MaxNoBlock`).
+
+[[ignoreSelfApproval]]
+* `ignoreSelfApproval` flag on labels:
++
+Labels can be configured to link:config-labels.html#label_ignoreSelfApproval[
+ignore self approvals]. This flag only works in combination with the deprecated
+label functions (see link:#label-functions[above]) and hence it is deprecated
+as well. Instead use a `submittableIf` expression with the
+link:#operator_label[label] operator and the `user=non_uploader` argument. See
+the link:#code-review-example[Code Review] submit requirement example.
+
+[[prolog-rules]]
+* Prolog rules:
++
+Projects can define link:prolog-cookbook.html[prolog submit rules] that control
+when changes can be submitted. It's still possible to have Prolog submit rules,
+but they are deprecated and support for them will be dropped in future Gerrit
+releases. Hence it's recommended to use submit requirements instead.
+
+When checking whether changes can be submitted Gerrit takes results of label
+functions and Prolog submit rules into account, in addition to the submit
+requirements.
+
+[[plugin-submit-rules]]
+== Plugin provided submit rules
+
+Plugins can contribute submit rules by implementing the `SubmitRule` extension
+point (see link:dev-plugins.html#pre-submit-evaluator[Pre-submit Validation
+Plugins]).
+
+When checking whether changes can be submitted Gerrit takes results of
+plugin-provided submit rules into account, in addition to the submit
+requirements.
+
+[[evaluation]]
+== Submit Requirement Evaluation
+
+Submit requirements are evaluated whenever a change is updated. To decide
+whether changes can be submitted, the results of link:#label-functions[label
+functions], link:#prolog-rules[Prolog submit rules] and
+link:#plugin-submit-rules[plugin-provided submit rules] are taken into account,
+in addition to the submit requirements. For this the results of label
+functions, Prolog submit rules and plugin-provided submit rules are converted
+to submit requirement results.
+
+Submit requirement results are returned in the REST API when retrieving changes
+with the link:rest-api-changes.html#submit-requirements[SUBMIT_REQUIREMENTS]
+option (e.g. via the link:rest-api-changes.html#get-change-detail[Get Change
+Detail] REST endpoint or the link:rest-api-changes.html#list-changes[Query
+Changes] REST endpoint). If requested, submit requirements are included as
+link:rest-api-changes.html#submit-requirement-result-info[
+SubmitRequirementResultInfo] entities into
+link:rest-api-changes.html#change-info[ChangeInfo] (field
+`submit_requirements`).
+
+The `status` field of submit requirement results can be one of:
+
+[[status-not-applicable]]
+* `NOT_APPLICABLE`
++
+The link:#submit_requirement_applicable_if[applicableIf] expression evaluates
+to false for the change.
+
+[[status-unsatisfied]]
+* `UNSATISFIED`
++
+The submit requirement is applicable
+(link:#submit_requirement_applicable_if[applicableIf] evaluates to true), but
+the evaluation of the link:#submit_requirement_submittable_if[submittableIf] and
+link:#submit_requirement_override_if[overrideIf] expressions return false for
+the change.
+
+[[status-satisfied]]
+* `SATISFIED`
++
+The submit requirement is applicable
+(link:#submit_requirement_applicable_if[applicableIf] evaluates to true), the
+link:#submit_requirement_submittable_if[submittableIf] expression evaluates to
+true, and the link:#submit_requirement_override_if[overrideIf] evaluates to
+false for the change.
+
+[[status-overridden]]
+* `OVERRIDDEN`
++
+The submit requirement is applicable
+(link:#submit_requirement_applicable_if[applicableIf] evaluates to true) and the
+link:#submit_requirement_override_if[overrideIf] expression evaluates to true.
++
+Note that in this case, the submit requirement is overridden regardless of
+whether the link:#submit_requirement_submittable_if[submittableIf] expression
+evaluates to true or not.
+
+[[status-forced]]
+* `FORCED`
++
+The change was merged directly bypassing code review by supplying the
+link:user-upload.html#auto_merge[submit] push option while doing a git push.
+
+[[status-error]]
+* `ERROR`
++
+The evaluation of any of the
+link:#submit_requirement_applicable_if[applicableIf],
+link:#submit_requirement_submittable_if[submittableIf] or
+link:#submit_requirement_override_if[overrideIf] expressions resulted in an
+error, i.e. because the expression is not parseable.
+
+[NOTE]
+Gerrit can be configured to return a `500 internal server error` response
+instead of setting the status to `ERROR` (see the
+link:config-gerrit.html#change.propagateSubmitRequirementErrors[
+change.propagateSubmitRequirementErrors] option that can be set in
+`gerrit.config`).
+
+[[submit-requirement-subsection]]
+== submit-requirement subsection
+
+Each `submit-requirement` subsection defines a submit requirement.
+
+The name of the `submit-requirement` subsection defines the name that uniquely
+identifies the submit requirement. It is shown to the user in the web UI when
+the submit requirement is applicable.
+
+[NOTE]
+By using the same name as an inherited submit requirement, the inherited submit
+requirement can be overridden, if overriding is allowed (see
+link:#submit_requirement_can_override_in_child_projects[
+canOverrideInChildProjects] field). Details about overriding submit
+requirements are explained in the link:#inheritance[inheritance] section.
+
+Submit requirements must at least define a
+link:#submit_requirement_submittable_if[submittableIf] expression that defines
+when a change can be submitted.
+
+.Example:
+----
+[submit-requirement "Verified"]
+  description = CI result status for build and tests is passing
+  applicableIf = -branch:refs/meta/config
+  submittableIf = label:Verified=MAX AND -label:Verified=MIN
+  canOverrideInChildProjects = true
+----
+
+The fields that can be set for submit requirements are explained below.
 
 [[submit_requirement_description]]
 === submit-requirement.Name.description
 
 A detailed description of what the submit requirement is supposed to do. This
-field is optional. The description is visible to the users in the change page
+field is optional. The description is visible to the user in the change page
 upon hovering on the submit requirement to help them understand what the
 requirement is about and how it can be fulfilled.
 
@@ -34,18 +377,21 @@
 === submit-requirement.Name.applicableIf
 
 A link:#query_expression_syntax[query expression] that determines if the submit
-requirement is applicable for a change. For example, administrators can exclude
-submit requirements for certain branch patterns. See the
-link:#exempt-branch-example[exempt branch] example.
+requirement is applicable for a change. If a submit requirement is not
+applicable it is hidden in the web UI. For example, this allows to
+link:#exempt-branch-example[exempt a branch] from the submit requirement.
 
+[TIP]
+--
 Often submit requirements should only apply to branches that contain source
-code. In this case this parameter can be used to exclude the
+code. In this case the `applicableIf` condition can be used to exclude the
 link:config-project-config.html#refs-meta-config[refs/meta/config] branch from
-a submit requirement:
+the submit requirement:
 
 ----
   applicableIf = -branch:refs/meta/config
 ----
+--
 
 This field is optional, and if not specified, the submit requirement is
 considered applicable for all changes in the project.
@@ -54,7 +400,7 @@
 === submit-requirement.Name.submittableIf
 
 A link:#query_expression_syntax[query expression] that determines when the
-change becomes submittable. This field is mandatory.
+change can be submitted. This field is mandatory.
 
 
 [[submit_requirement_override_if]]
@@ -64,9 +410,10 @@
 submit requirement is overridden. When this expression is evaluated to true,
 the submit requirement state becomes `OVERRIDDEN` and the submit requirement
 is no longer blocking the change submission.
+
 This expression can be used to enable bypassing the requirement in some
-circumstances, for example if the change owner is a power user or to allow
-change submission in case of emergencies. +
+circumstances, for example if the uploader is a trusted bot user or to allow
+change submission in case of emergencies.
 
 This field is optional.
 
@@ -74,73 +421,10 @@
 === submit-requirement.Name.canOverrideInChildProjects
 
 A boolean (true, false) that determines if child projects can override the
-submit requirement. +
+submit requirement.
 
 The default value is `false`.
 
-[[evaluation_results]]
-== Evaluation Results
-
-When submit requirements are configured, their results are returned for all
-changes requested by the REST API with the
-link:rest-api-changes.html#submit-requirement-result-info[SubmitRequirementResultInfo]
-entity. +
-
-Submit requirement results are produced from the evaluation of the submit
-requirements in the project config (
-See link:#configuring_submit_requirements[Configuring Submit Requirements])
-as well as the conversion of the results of the legacy submit rules to submit
-requirement results. Legacy submit rules are label functions
-(see link:config-labels.html[config labels]), custom and
-link:prolog-cookbook.html[prolog] submit rules.
-
-The `status` field can be one of:
-
-* `NOT_APPLICABLE`
-+
-The link:#submit_requirement_applicable_if[applicableIf] expression evaluates
-to false for the change.
-
-* `UNSATISFIED`
-+
-The submit requirement is applicable
-(link:#submit_requirement_applicable_if[applicableIf] evaluates to true), but
-the evaluation of the link:#submit_requirement_submittable_if[submittableIf] and
-link:#submit_requirement_override_if[overrideIf] expressions return false for
-the change.
-
-* `SATISFIED`
-+
-The submit requirement is applicable
-(link:#submit_requirement_applicable_if[applicableIf] evaluates to true), the
-link:#submit_requirement_submittable_if[submittableIf] expression evaluates to
-true, and the link:#submit_requirement_override_if[overrideIf] evaluates to
-false for the change.
-
-* `OVERRIDDEN`
-+
-The submit requirement is applicable
-(link:#submit_requirement_applicable_if[applicableIf] evaluates to true) and the
-link:#submit_requirement_override_if[overrideIf] expression evaluates to true.
-+
-Note that in this case, the change is overridden whether the
-link:#submit_requirement_submittable_if[submittableIf] expression evaluates to
-true or not.
-
-* `FORCED`
-+
-The change was merged directly bypassing code review by supplying the
-link:user-upload.html#auto_merge[submit] push option while doing a git push.
-
-* `ERROR`
-+
-The evaluation of any of the
-link:#submit_requirement_applicable_if[applicableIf],
-link:#submit_requirement_submittable_if[submittableIf] or
-link:#submit_requirement_override_if[overrideIf] expressions resulted in an
-error.
-
-
 [[query_expression_syntax]]
 == Query Expression Syntax
 
@@ -247,13 +531,17 @@
 pattern.
 
 [[operator_label]]
-label:labelName=+1,user=non_contributor::
+label:LabelExpression::
 +
-Submit requirements support an additional `user=non_contributor` argument for
-labels that returns true if the change has a label vote matching the specified
-value and the vote is applied from a gerrit account that's not the uploader,
-author or committer of the latest patchset. See the documentation for the labels
-operator in the link:user-search.html[user search] page.
+The `label` operator allows to match changes that have votes matching the given
+`LabelExpression`. The `LabelExpression` can be anything that's supported for
+the link:user-search.html#labels[label] query operator.
++
+If used in submit requirement expressions, this operator supports an additional
+`user=non_contributor` argument. This argument works similar to the
+link:user-search.html#non_uploader["user=non_uploader"] argument and returns
+true if the change has a matching label vote that is applied by a user that's
+not the uploader, author or committer of the latest patchset.
 
 [[unsupported_operators]]
 === Unsupported Operators
@@ -265,39 +553,6 @@
 +
 Cannot be used since it will result in recursive evaluation of expressions.
 
-[[inheritance]]
-== Inheritance
-
-Child projects can override a submit requirement defined in any of their parent
-projects. Overriding a submit requirement overrides all of its properties and
-values. The overriding project needs to define all mandatory fields.
-
-Submit requirements are looked up from the current project up the inheritance
-hierarchy to “All-Projects”. The first project in the hierarchy chain that sets
-link:#submit_requirement_can_override_in_child_projects[canOverrideInChildProjects]
-to false prevents all descendant projects from overriding it.
-
-If a project disallows a submit requirement from being overridden in child
-projects, all definitions of this submit requirement in descendant projects are
-ignored.
-
-To remove a submit requirement in a child project, administrators can redefine
-the requirement with the same name in the child project and set the
-link:#submit_requirement_applicable_if[applicableIf] expression to `is:false`.
-Since the link:#submit_requirement_submittable_if[submittableIf] field is
-mandatory, administrators need to provide it in the child project but can set it
-to anything, for example `is:false` but it will have no effect anyway.
-
-
-[[trigger-votes]]
-== Trigger Votes
-
-Trigger votes are label votes that are not associated with any submit
-requirement expressions. Trigger votes are displayed in a separate section in
-the change page. For more about configuring labels, see the
-link:config-labels.html[config labels] documentation.
-
-
 [[examples]]
 == Examples
 
@@ -359,46 +614,6 @@
   submittableIf = hasfooter:\"Bug\"
 ----
 
-[[test-submit-requirements]]
-== Testing Submit Requirements
-
-The link:rest-api-changes.html#check-submit-requirement[Check Submit Requirement]
-change endpoint can be used to test submit requirements on any change. Users
-are encouraged to test submit requirements before adding them to the project
-to ensure they work as intended.
-
-.Request
-----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-    {
-      "name": "Code-Review",
-      "submittability_expression": "label:Code-Review=+2"
-    }
-----
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "name": "Code-Review",
-    "status": "SATISFIED",
-    "submittability_expression_result": {
-      "expression": "label:Code-Review=+2",
-      "fulfilled": true,
-      "passingAtoms": [
-        "label:Code-Review=+2"
-      ]
-    },
-    "is_legacy": false
-  }
-----
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 53d9e6b..4c224b5 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -393,6 +393,34 @@
   bazelisk test //plugins/replication/...
 ----
 
+[[known-issues]]
+=== Known Issues
+
+[[byte-buddy-not-initialized-or-unavailable]]
+==== The Byte Buddy agent is not initialized or unavailable
+
+If running tests that make use of mocks fail with the exception below, set the
+`sandbox_tmpfs_path` flag for running tests in `.bazelrc` as described in this
+link:https://github.com/mockito/mockito/issues/1879#issuecomment-922459131[
+issue], e.g. add this line: `test --sandbox_tmpfs_path=/tmp`
+
+.Exception:
+----
+...
+Caused by: org.mockito.exceptions.base.MockitoInitializationException:
+Could not initialize inline Byte Buddy mock maker.
+
+It appears as if your JDK does not supply a working agent attachment mechanism.
+...
+Caused by: java.lang.IllegalStateException: The Byte Buddy agent is not initialized or unavailable
+at net.bytebuddy.agent.ByteBuddyAgent.getInstrumentation(ByteBuddyAgent.java:230)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:617)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:568)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:545)
+at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<clinit>(InlineDelegateByteBuddyMockMaker.java:115)
+... 47 more
+----
+
 [[debugging-tests]]
 == Debugging Unit Tests
 In some cases it may be necessary to debug a test while running it in bazel. For example, when we
@@ -664,26 +692,16 @@
     --disk-size=200
 ```
 
-Due to outdated Git version in official RBE docker images, a custom RBE docker
-image must be used. To build custom docker imager, change to the directory
-`tools/platforms` and build and publish custom RBE docker image.
+Note, that we are using Ubuntu2204 docker image from bazel project:
 
-To build the custom RBE docker image, run:
 
 ```
-docker build -t gcr.io/api-project-164060093628/rbe-ubuntu18-04 .
+docker pull gcr.io/bazel-public/ubuntu2204-java17@sha256:ffe37746a34537d8e73cef5a20ccd3a4e3ec7af3e7410cba87387ba97c0e520f
 ```
 
-To publish the custom RBE docker image, run:
-
-```
-docker push gcr.io/api-project-164060093628/rbe-ubuntu18-04
-[...]
-latest: digest: sha256:de5186d4313630a6111f9a2449b72563d0bc59ec9fb60956f063b69a38a76834 size: 1584
-```
-
-Re-build rbe_autoconfig project conduct a new release and switch to using it
-in `WORKSPACE` file.
+Re-build rbe_autoconfig project, conduct a new release and switch to using it
+in `WORKSPACE` file. For more details see this
+link:https://github.com/davido/rbe_autoconfig[repository,role=external,window=_blank]
 
 Note, to authenticate to the gcr.io registry, the following command must be
 used:
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
index 1229faf..07e3a11 100644
--- a/Documentation/dev-community.txt
+++ b/Documentation/dev-community.txt
@@ -45,7 +45,7 @@
 * link:dev-readme.html[Developer Setup]
 * link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui[TypeScript Frontend Developer Setup]
 * link:dev-crafting-changes.html[Crafting Changes]
-* link:dev-starter-projects.html[Starter Projects]
+* link:dev-contribution-opportunities.html[Contribution Opportunities (Help Wanted)]
 
 [[plugin-development]]
 == Plugin Development
diff --git a/Documentation/dev-contribution-opportunities.txt b/Documentation/dev-contribution-opportunities.txt
new file mode 100644
index 0000000..23f916e
--- /dev/null
+++ b/Documentation/dev-contribution-opportunities.txt
@@ -0,0 +1,45 @@
+:linkattrs:
+= Gerrit Code Review - Contribution Opportunities
+
+If you are eager to contribute to Gerrit, but you don't know where to
+start here are some opportunities to contribute.
+
+[[help-wanted]]
+== Help Wanted
+
+The link:https://issues.gerritcodereview.com/hotlists/5395287[HelpWanted,role=external,window=_blank]
+hotlist in the issue tracker lists issues that need help from the
+community and where any contribution is very welcome.
+
+If you are interested in any of the projects and you want to try
+implementing it, just assign the corresponding issue to yourself and
+reach out on the
+link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list,role=external,window=_blank]
+if you have questions or need help.
+
+[[creating-help-wanted-issue]]
+=== Creating Help Wanted issues
+
+Issues on the link:https://issues.gerritcodereview.com/hotlists/5395287[HelpWanted,role=external,window=_blank]
+hotlist should be phrased as user stories and be well-scoped so that they can
+be easily picked up by new contributors:
+
+* The issue title should name the feature and be prefixed with a t-shirt size
+  in square brackets to indicate the expected effort.
+* The issue description should use Markdown and have the following sections:
+** *User Story*: Explain the use case that is being addressed and what's the
+   value of the feature.
+** *What*: Describe what should be done.
+** *Background*: Any useful background information, including ideas how the
+   feature can be done.
+** *Pointers*: Useful links to documentation and code
+
+Note, only link:dev-roles.html#maintainer[maintainers] and
+link:dev-roles.html#contributor[trusted contributors] can add issues to the `HelpWanted` hotlist.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 42edc1f..bcc96b4 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2248,6 +2248,52 @@
 e.g. a plugin can provide a list of servers on which the change was
 deployed.
 
+Plugins can filter the branches and tags that are inlcuded by implementing
+`com.google.gerrit.server.change.FilterIncludedIn`.
+
+[source, java]
+----
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.change.FilterIncludedIn;
+import java.util.function.Predicate;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+public class MyFilter implements FilterIncludedIn {
+  @Override
+  public Predicate<String> getBranchFilter(Project.NameKey project, RevCommit commit) {
+    if (project.get() != "myproject") {
+      return branch -> true;
+    }
+    return branch -> !branch.startsWith("feature/");
+  }
+
+  @Override
+  public Predicate<String> getTagFilter(Project.NameKey project, RevCommit commit) {
+    if (project.get() != "myproject") {
+      return tag -> true;
+    }
+    return tag -> tag.startsWith("v");
+  }
+}
+----
+
+And register your class:
+
+[source, java]
+----
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.change.FilterIncludedIn;
+import com.google.inject.AbstractModule;
+
+public class MyPluginModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), FilterIncludedIn.class).to(MyFilter.class);
+  }
+}
+----
+
 [[change-report-formatting]]
 == Change Report Formatting
 
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 8b7ba4b..c2a1f86 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -208,19 +208,14 @@
 
 To report a security vulnerability file a
 link:https://issues.gerritcodereview.com/issues/new?component=1371046[
-security issue,role=external,window=_blank] in the Gerrit issue tracker. The visibility of issues that are
-created with the `Security Issue` template is automatically restricted to
-Gerrit maintainers and a few long-term contributors. This means as a reporter
-you may not be able to see the issue once it is created. Security issues are
-created on the `ESC` component so that they will be discussed at the next
-meeting of the link:#steering-committee[Engineering Steering Committee] which
-takes place biweekly.
+security issue,role=external,window=_blank] in the Gerrit issue tracker. Issues
+in the `Gerrit Code Review > Security` component are restricted to Gerrit
+maintainers and a few long-term contributors. The reporter becomes a
+collaborator on the issue and hence can see it as well. Security issues are
+triaged by the link:#steering-committee[Engineering Steering Committee].
 
-If an existing issue is found to be a security vulnerability it should be
-turned into a security issue by:
-
-. Setting the component to `ESC`
-. Adding the labels `Security` and `NonPublic`
+If an existing issue is found to be a security vulnerability it should be moved
+to `Gerrit Code Review > Security` component (component ID: 1371046).
 
 In case of doubt, or if an issue cannot wait until the next ESC meeting,
 contact the link:#steering-committee[Engineering Steering Committee] directly
@@ -370,6 +365,12 @@
 link:https://gerrit-review.googlesource.com/q/label:%2522Library-Compliance%253Dneed%2522+-ownerin:google-gerrit-team+status:open+project:gerrit+-age:4week+-is:wip+-is:private+label:Code-Review%252B2[change query]
 to find changes that require a `Library-Compliance` approval.
 
+To get the attention of a Googler for dependency updates file separate issues
+(use type "Task") for each dependency update on the
+link:https://issues.gerritcodereview.com/issues/new?component=1371020&template=1834212["Hosting > googlesource" component].
+Then it will show up in Google's triage queue and the current person who is on duty
+should look into this.
+
 Gerrit's library dependencies should only be upgraded if the new version contains
 something we need in Gerrit. This includes new features, API changes as well as bug
 or security fixes.
diff --git a/Documentation/dev-starter-projects.txt b/Documentation/dev-starter-projects.txt
deleted file mode 100644
index 92de84d..0000000
--- a/Documentation/dev-starter-projects.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-:linkattrs:
-= Gerrit Code Review - Starter Projects
-
-We have created a
-link:https://issues.gerritcodereview.com/hotlists/5052926[StarterProject,role=external,window=_blank]
-hotlist in the issue tracker and try to assign easy hack projects to it. If in
-doubt, do not hesitate to ask on the developer
-link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list,role=external,window=_blank].
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/images/generated-suggested-edit-added.png b/Documentation/images/generated-suggested-edit-added.png
new file mode 100644
index 0000000..37300c3
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-added.png
Binary files differ
diff --git a/Documentation/images/generated-suggested-edit-preview.png b/Documentation/images/generated-suggested-edit-preview.png
new file mode 100644
index 0000000..9ca82f1
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-preview.png
Binary files differ
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 97b58af..15f27db 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -44,7 +44,7 @@
 To see the access rights of your project
 
 - go to the Gerrit Web UI
-- click on the `Projects` > `List` menu entry
+- click on the `BROWSE` > `Repositories` menu entry
 - find your project in the project list and click on it
 - click on the `Access` menu entry
 
@@ -150,7 +150,7 @@
 group backends.
 
 The Gerrit internal groups can be seen in the Gerrit Web UI by clicking
-on the `Groups` > `List` menu entry. By clicking on a group you can
+on the `BROWSE` > `Groups` menu entry. By clicking on a group you can
 edit the group members (`Members` tab) and the group options
 (`General` tab).
 
@@ -168,8 +168,9 @@
 `Make group visible to all registered users.`, which defines whether
 non-members can see who is member of the group.
 
-New internal Gerrit groups can be created under `Groups` >
-`Create New Group`. This menu is only available if you have the global
+New internal Gerrit groups can be created under `BROWSE` > `Groups`
+and then clicking on the `CREATE NEW` button in the upper right corner.
+The `CREATE NEW` button is only available if you have the global
 capability link:access-control.html#capability_createGroup[Create Group]
 assigned.
 
@@ -238,7 +239,7 @@
 To see the options of your project:
 
 . Go to the Gerrit Web UI.
-. Click on the `Projects` > `List` menu entry.
+. Click on the `BROWSE` > `Repositories` menu entry.
 . Find your project in the project list and click it.
 . Click the `General` menu entry.
 
@@ -431,7 +432,7 @@
 == Branch Administration
 
 As project owner you can administrate the branches of your project in
-the Gerrit Web UI under `Projects` > `List` > <your project> >
+the Gerrit Web UI under `BROWSE` > `Repositories` > <your project> >
 `Branches`. In the Web UI link:project-configuration.html#branch-creation[
 branch creation] is allowed if you have
 link:access-control.html#category_create[Create Reference] access right and
@@ -450,7 +451,7 @@
 
 With Gerrit individual users control their own email subscriptions. By
 editing the link:user-notify.html#user[watched projects] in the Web UI
-under `Settings` > `Watched Projects` users can decide which events to
+under `Settings` > `Notifications` users can decide which events to
 be informed about by email. The change notifications can be filtered by
 link:user-search.html[change search expressions].
 
@@ -476,8 +477,8 @@
 link:user-dashboards.html#project-dashboards[project level]. This way
 you can define a view of the changes that are relevant for your
 project and share this dashboard with all users. The project dashboards
-can be seen in the Web UI under `Projects` > `List` > <your project> >
-`Dashboards`.
+can be seen in the Web UI under `BROWSE` > `Repositories` > <your project>
+> `Dashboards`.
 
 [[issue-tracker-integration]]
 == Issue Tracker Integration
@@ -509,7 +510,7 @@
 to Gerrit changes to the issues in the issue tracker system or to
 automatically close an issue if the corresponding change is merged.
 If installed, project owners may enable/disable the issue tracker
-integration from the Gerrit Web UI under `Projects` > `Lists` >
+integration from the Gerrit Web UI under `BROWSE` > `Repositories` >
 <your project> > `General`.
 
 [[comment-links]]
@@ -554,7 +555,7 @@
 With the link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers[
 reviewers,role=external,window=_blank] plugin it is possible to configure default reviewers who
 will be automatically added to each change. The default reviewers can
-be configured in the Gerrit Web UI under `Projects` > `List` >
+be configured in the Gerrit Web UI under `BROWSE` > `Repositories` >
 <your project> > `General` in the `reviewers Plugin` section.
 
 The link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers-by-blame[
@@ -565,7 +566,7 @@
 touched by the change, since these users should be familiar with the
 code and can most likely review the change. How many reviewers the
 plugin will add to a change at most can be configured in the Gerrit
-Web UI under `Projects` > `List` > <your project> > `General` in the
+Web UI under `BROWSE` > `Repositories` > <your project> > `General` in the
 `reviewers-by-blame Plugin` section.
 
 [[download-commands]]
@@ -650,9 +651,10 @@
 [[project-creation]]
 === Project Creation
 
-New projects can be created in the Gerrit Web UI under `Projects` >
-`Create Project`. The `Create Project` menu entry is only available if
-you have the link:access-control.html#capability_createProject[
+New projects can be created in the Gerrit Web UI under `BROWSE` >
+`Repositories` and then clicking on the `CREATE NEW` button in the
+upper right corner. The `CREATE NEW` button is only available if you
+have the link:access-control.html#capability_createProject[
 Create Project] global capability assigned.
 
 Projects can also be created via REST or SSH as described in the
@@ -736,7 +738,7 @@
 
 If the link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[
 delete-project,role=external,window=_blank] plugin is installed, projects can be deleted from the
-Gerrit Web UI under `Projects` > `List` > <project> > `General` by
+Gerrit Web UI under `BROWSE` > `Repositories` > <project> > `General` by
 clicking on the `Delete` command under `Project Commands`. The `Delete`
 command is only available if you have the `Delete Projects` global
 capability assigned, or if you own the project and you have the
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 7825e050..108022a9 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -28,15 +28,10 @@
 Still there are some client-side tools for Gerrit, which can be used
 optionally:
 
-* link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector,role=external,window=_blank]: Gerrit
-  integration with Mylyn
 * link:https://github.com/uwolfer/gerrit-intellij-plugin[Gerrit
   IntelliJ Plugin,role=external,window=_blank]: Gerrit integration with the
   link:http://www.jetbrains.com/idea/[IntelliJ Platform,role=external,window=_blank]
-* link:https://play.google.com/store/apps/details?id=com.jbirdvegas.mgerrit[
-  mGerrit,role=external,window=_blank]: Android client for Gerrit
-* link:https://github.com/stackforge/gertty[Gertty,role=external,window=_blank]: Console-based
-  interface for Gerrit
+* link:https://opendev.org/ttygroup/gertty[gertty]: Console-based interface for Gerrit
 
 [[clone]]
 == Clone Gerrit Project
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 6c3c459..7e3fa9a 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -77,7 +77,8 @@
 * jetty:util-ajax
 * log:log4j
 * lucene:lucene-analyzers-common
-* lucene:lucene-core-and-backward-codecs-merged
+* lucene:lucene-backward-codecs
+* lucene:lucene-core
 * lucene:lucene-misc
 * lucene:lucene-queryparser
 * mime4j:core
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index df0cc42b..4302a35 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -430,6 +430,13 @@
 * `permissions/ref_filter/skip_filter_count`: Rate of ref filter operations
   where we skip full evaluation because the user can read all refs
 
+=== Validation
+
+* `validation/file_count`: Track number of files per change during commit
+  validation, if it exceeds the FILE_COUNT_WARNING_THRESHOLD threshold.
+** `file_count`: number of files in the patchset
+** `host_repo`: host and repository of the change in the format 'host/repo'
+
 === Reviewer Suggestion
 
 * `reviewer_suggestion/query_accounts`: Latency for querying accounts for
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 9ecef3f..d0c4553 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -377,7 +377,7 @@
   "Out Of Office"
 ----
 
-If the name was deleted the response is "`204 No Content`".
+If the status was deleted the response is "`204 No Content`".
 
 [[get-username]]
 === Get Username
@@ -449,6 +449,8 @@
 
 As response the new display name is returned.
 
+If the Display Name was deleted the response is "`204 No Content`".
+
 [[get-active]]
 === Get Active
 --
@@ -1314,6 +1316,7 @@
     "publish_comments_on_push": true,
     "work_in_progress_by_default": true,
     "allow_browser_notifications": true,
+    "allow_suggest_code_while_commenting": true,
     "diff_page_sidebar": "plugin-foo",
     "default_base_for_merges": "FIRST_PARENT",
     "my": [
@@ -1368,6 +1371,7 @@
     "disable_keyboard_shortcuts": true,
     "disable_token_highlighting": true,
     "allow_browser_notifications": false,
+    "allow_suggest_code_while_commenting": false,
     "diff_page_sidebar": "NONE",
     "diff_view": "SIDE_BY_SIDE",
     "mute_common_path_prefixes": true,
@@ -2716,6 +2720,9 @@
 inline edit feature.
 |`allow_browser_notifications`  |not set if `false`|
 Whether to prompt user to enable browser notification in browser.
+|`allow_suggest_code_while_commenting`  |not set if `false`|
+Whether to receive suggested code while writing comments. This feature needs
+a plugin implementation.
 |`diff_page_sidebar`            |optional|
 String indicating which sidebar should be open on the diff page. Set to "NONE"
 if no sidebars should be open. Plugin-supplied sidebars will be prefixed with
@@ -2791,6 +2798,9 @@
 inline edit feature.
 |`allow_browser_notifications`  |not set if `false`|
 Whether to prompt user to enable browser notification in browser.
+|`allow_suggest_code_while_commenting`  |not set if `false`|
+Whether to receive suggested code while writing comments. This feature needs
+a plugin implementation.
 |`diff_page_sidebar`            |optional|
 String indicating which sidebar should be open on the diff page. Set to "NONE"
 if no sidebars should be open. Plugin-supplied sidebars will be prefixed with
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a56766e..4ed05bd 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -63,7 +63,8 @@
     "_number": 4711,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -126,6 +127,7 @@
       "owner": {
         "name": "John Doe"
       },
+      "current_revision_number": 2
     },
     {
       "id": "demo~master~I09c8041b5867d5b33170316e2abc34b79bbb8501",
@@ -143,6 +145,7 @@
       "owner": {
         "name": "John Doe"
       },
+      "current_revision_number": 2,
       "_more_changes": true
     }
   ]
@@ -214,7 +217,8 @@
         "labels": {
           "Verified": {},
           "Code-Review": {}
-        }
+        },
+        "current_revision_number": 2
       }
     ],
     [],
@@ -437,6 +441,7 @@
       "owner": {
         "name": "Shawn Pearce"
       },
+      "current_revision_number": 1,
       "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
       "revisions": {
         "184ebe53805e102605d11f6b143486d15c23a09c": {
@@ -598,7 +603,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -680,7 +686,8 @@
       "_number": 3965,
       "owner": {
         "name": "John Doe"
-      }
+      },
+      "current_revision_number": 2
     },
     "new_change_info": {
       "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
@@ -709,7 +716,8 @@
       "_number": 3965,
       "owner": {
         "name": "John Doe"
-      }
+      },
+      "current_revision_number": 2
     },
   }
 ----
@@ -978,7 +986,8 @@
         "message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
         "_revision_number": 1
       }
-    ]
+    ],
+    "current_revision_number": 2
   }
 ----
 
@@ -1040,10 +1049,44 @@
     "owner": {
       "_account_id": 1000000
     },
+    "current_revision_number": 1,
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
   }
 ----
 
+[[get-message]]
+=== Get Commit Message
+--
+'GET /changes/link:#change-id[\{change-id\}]/message'
+--
+
+Returns the commit message of the change (from the current patch set).
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
+----
+
+The commit message is returned as a link:#commit-message-info[
+CommitMessageInfo] entity.
+
+Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "subject": "Add feature X",
+    "full_message": "Add Feature X\n\nFeature X helps with foo.\n\nBug: 123\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n",
+    "footers": {
+      "Bug": "123",
+      "Change-Id": "I10394472cbd17dd12454f229e4f6de00b143a444"
+    }
+  }
+----
+
 [[set-message]]
 === Set Commit Message
 --
@@ -1235,7 +1278,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -1306,7 +1350,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -1331,6 +1376,9 @@
 
 Rebases a change.
 
+For merge commits always the first parent is rebased. This means the new base becomes the first
+parent of the rebased merge commit while the second parent stays intact.
+
 If one of the secondary emails associated with the user performing the operation was used as the
 committer email in the current patch set, the same email will be used as the committer email in the
 new patch set; otherwise, the user's preferred email will be used.
@@ -1375,6 +1423,7 @@
     "owner": {
       "name": "John Doe"
     },
+    "current_revision_number": 2,
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
@@ -1525,6 +1574,7 @@
         "owner": {
           "_account_id": 1000000
         },
+        "current_revision_number": 2,
         "current_revision": "c3b2ba222d42a56e05c90f88d4509a124620517d",
         "revisions": {
           "c3b2ba222d42a56e05c90f88d4509a124620517d": {
@@ -1602,6 +1652,7 @@
         "owner": {
           "_account_id": 1000000
         },
+        "current_revision_number": 2,
         "current_revision": "77eb17a9501a5c21963bc6af56085e60f281acbb",
         "revisions": {
           "77eb17a9501a5c21963bc6af56085e60f281acbb": {
@@ -1726,7 +1777,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -1805,7 +1857,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -1899,7 +1952,8 @@
         "_number": 3965,
         "owner": {
           "name": "John Doe"
-        }
+        },
+        "current_revision_number": 2
       },
       {
         "id": "anyProject~master~1eee2c9d8f352483781e772f35dc586a69ff5646",
@@ -1917,7 +1971,8 @@
         "_number": 3966,
         "owner": {
           "name": "Jane Doe"
-        }
+        },
+        "current_revision_number": 2
       }
     ]
 ----
@@ -2002,7 +2057,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -2155,6 +2211,7 @@
           }
         ]
       },
+      "current_revision_number": 1,
       "current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
       "revisions": {
         "9adb9f4c7b40eeee0646e235de818d09164d7379": {
@@ -2252,6 +2309,7 @@
           }
         ]
       },
+      "current_revision_number": 1,
       "current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
       "revisions": {
         "1bd7c12a38854a2c6de426feec28800623f492c4": {
@@ -2361,6 +2419,7 @@
     "owner": {
       "name": "John Doe"
     },
+    "current_revision_number": 1,
     "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c"
   }
 ----
@@ -2477,7 +2536,7 @@
 ----
 
 [[list-change-robot-comments]]
-=== List Change Robot Comments
+=== List Change Robot Comments (deprecated)
 --
 'GET /changes/link:#change-id[\{change-id\}]/robotcomments'
 --
@@ -2625,6 +2684,7 @@
     "owner": {
       "name": "John Doe"
     },
+    "current_revision_number": 2,
     "problems": [
       {
         "message": "Current patch set 1 not found"
@@ -2677,6 +2737,7 @@
     "owner": {
       "name": "John Doe"
     },
+    "current_revision_number": 2,
     "problems": [
       {
         "message": "Current patch set 2 not found"
@@ -3383,6 +3444,36 @@
   HTTP/1.1 204 No Content
 ----
 
+[[put-change-edit-committer-author-identity]]
+=== Change author or committer identity in Change Edit
+--
+'PUT /changes/link:#change-id[\{change-id\}]/edit:identity'
+--
+
+Modify author or committer identity. The request body needs to include a
+link:#change-edit-identity-input[ChangeEditIdentityInput]
+entity. Either `name` or `email` must be provided. `type` must be either `AUTHOR` or `COMMITTER`.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:identity HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "name": "John Doe",
+    "email": "john.doe@example.com",
+    "type": "COMMITTER"
+  }
+----
+
+If a change edit doesn't exist for this change yet, it is created. As
+response "`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-edit-file]]
 === Retrieve file content from Change Edit
 --
@@ -3642,10 +3733,22 @@
 This REST endpoint only suggests accounts that
 
 * are active
+
 * can see the change
-* are visible to the calling user
+
+* are visible to the calling user:
++
+Whether an account is visible to the calling user depends on the
+link:config-gerrit.html#accounts.visibility[accounts.visibility] setting of the
+server. Which account visibility is configured can be checked by opening
+`https://<HOST>/config/server/info?pp=1` in a browser (see field `accounts` >
+link:rest-api-config.html#accounts-config-info[visibility] in the returned
+JSON).
+
 * are not already reviewer on the change
+
 * don't own the change
+
 * are not service users (unless
   link:config.html#suggest.skipServiceUsers[skipServiceUsers] is set to `false`)
 
@@ -4349,6 +4452,7 @@
         }
       ]
     },
+    "current_revision_number": 2,
     "current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
     "revisions": {
       "674ac754f91e64a0efb8087e59a176484bd534d1": {
@@ -4740,6 +4844,7 @@
     "owner": {
       "name": "John Doe"
     },
+    "current_revision_number": 2,
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
@@ -4840,7 +4945,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -5392,7 +5498,7 @@
 ----
 
 [[list-robot-comments]]
-=== List Robot Comments
+=== List Robot Comments (deprecated)
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
 --
@@ -5449,7 +5555,7 @@
 ----
 
 [[get-robot-comment]]
-=== Get Robot Comment
+=== Get Robot Comment (deprecated)
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/link:#comment-id[\{comment-id\}]'
 --
@@ -5689,7 +5795,7 @@
   Content-Disposition: attachment
   Content-Type: text/plain; charset=UTF-8
 
-  The existing change edit could not be merged with another tree.
+  Rebasing change edit onto another patchset results in merge conflicts. Download the edit patchset and rebase manually to preserve changes.
 ----
 
 [[apply-provided-fix]]
@@ -6413,7 +6519,8 @@
     "_number": 3965,
     "owner": {
       "name": "John Doe"
-    }
+    },
+    "current_revision_number": 2
   }
 ----
 
@@ -7021,6 +7128,19 @@
 |`message`     ||New commit message.
 |===========================
 
+[[change-edit-identity-input]]
+=== ChangeEditIdentityInput
+The `ChangeEditIdentityInput` entity contains information for changing
+the author or committer identity within a change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`name`     |optional|The name of the author/committer. If not specified, the existing name will be used.
+|`email`    |optional|The email of the author/committer. If not specified, the existing email will be used.
+|`type`     ||Type of the identity being edited. Must be either `AUTHOR` or `COMMITTER`.
+|===========================
+
 [[change-info]]
 === ChangeInfo
 The `ChangeInfo` entity contains information about a change.
@@ -7150,11 +7270,9 @@
 |`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.
-Possible reviewer states are `REVIEWER`, `CC` and `REMOVED`. +
+Possible reviewer states are `REVIEWER`, `CC`. +
 `REVIEWER`: Users with at least one non-zero vote on the change. +
 `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:#labels[labels] or
 link:#detailed-labels[detailed labels] are requested.
 |`pending_reviewers`  |optional|
@@ -7164,6 +7282,11 @@
 notified about being added to or removed from the change. +
 Only set if link:#labels[labels] or
 link:#detailed-labels[detailed labels] are requested.
+Possible states are `REVIEWER`, `CC` and `REMOVED`. +
+`REVIEWER`: Users with at least one non-zero vote on the change. +
+`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.
 |`reviewer_updates`|optional|
 Updates to reviewers set for the change as
 link:#review-update-info[ReviewerUpdateInfo] entities.
@@ -7172,6 +7295,8 @@
 Messages associated with the change as a list of
 link:#change-message-info[ChangeMessageInfo] entities. +
 Only set if link:#messages[messages] are requested.
+|`current_revision_number`||The number of the current patch set of this
+change. +
 |`current_revision`   |optional|
 The commit ID of the current patch set of this change. +
 Only set if link:#current-revision[the current revision] is requested
@@ -7471,7 +7596,8 @@
 Mime type of the file where the comment is written. Available only if the
 "enable-context" parameter (see link:#list-change-comments[List Change Comments])
 is set.
-
+|`fix_suggestions`|optional|Suggested fixes for this comment as a list of
+<<fix-suggestion-info,FixSuggestionInfo>> entities.
 |===========================
 
 [[comment-input]]
@@ -7519,6 +7645,8 @@
 Whether or not the comment must be addressed by the user. This value will
 default to false if the comment is an orphan, or the value of the `in_reply_to`
 comment if it is supplied.
+|`fix_suggestions`|optional|Suggested fixes for this comment as a list of
+<<fix-suggestion-info,FixSuggestionInfo>> entities.
 |===========================
 
 [[comment-range]]
@@ -7585,6 +7713,21 @@
 link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
+[[commit-message-info]]
+=== CommitMessageInfo
+The `CommitMessageInfo` entity contains information about the commit
+message of a change.
+
+[options="header",cols="1,6"]
+|============================
+|Field Name     |Description
+|`subject`      |The subject of the change (first line of the commit
+message).
+|`full_message` |Full commit message of the change.
+|`footers`      |The footers from the commit message as a map of
+key-value pairs.
+|============================
+
 [[commit-message-input]]
 === CommitMessageInput
 The `CommitMessageInput` entity contains information for changing
@@ -7594,6 +7737,9 @@
 |=============================
 |Field Name      ||Description
 |`message`       ||New commit message.
+|`committer_email`|optional|
+New message is committed using this email address. Only the
+registered emails of the calling user are considered valid.
 |`notify`        |optional|
 Notify handling that defines to whom email notifications should be sent
 after the commit message was updated. +
@@ -8527,8 +8673,10 @@
 The account which modified state of the reviewer in question as
 link:rest-api-accounts.html#account-info[AccountInfo] entity.
 |`reviewer`|
-The reviewer account added or removed from the change as an
-link:rest-api-accounts.html#account-info[AccountInfo] entity.
+The reviewer added or removed from the change as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity. For
+reviewers by email the `AccountInfo` doesn't contain an account ID but
+only the email and optionally a name.
 |`state`|
 The reviewer state, one of `REVIEWER`, `CC` or `REMOVED`.
 |===========================
@@ -8557,7 +8705,7 @@
 |`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, deprecated|
 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|
@@ -8636,9 +8784,8 @@
 action. Not set if false.
 |`error`                  |optional|
 Error message for non-200 responses.
-|`change_info`            |optional|
-Post-update change information. Only set if `response_format_options` were
-set.
+|`change_info`            ||
+Post-update change information.
 |============================
 
 [[reviewer-info]]
@@ -8807,7 +8954,7 @@
 |===========================
 
 [[robot-comment-info]]
-=== RobotCommentInfo
+=== RobotCommentInfo (deprecated)
 The `RobotCommentInfo` entity contains information about a robot inline
 comment.
 
@@ -8823,12 +8970,10 @@
 |`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.
 |===========================
 
 [[robot-comment-input]]
-=== RobotCommentInput
+=== RobotCommentInput (deprecated)
 The `RobotCommentInput` entity contains information for creating an inline
 robot comment.
 
@@ -8858,8 +9003,6 @@
 |`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]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index ec1ac03..37121350 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -875,6 +875,49 @@
   }
 ----
 
+[[list-experiments]]
+=== List Experiments
+--
+'GET /config/server/experiments'
+--
+
+Lists the experiments that are available in the system.
+
+Requires the caller to have the link:access-control.html#capability_administrateServer[
+Administrate Server] global capability.
+
+As result a map of experiment names to link:#experiment-info[Experiment] entities is returned.
+
+The entries in the map are sorted by experiment name.
+
+.Request
+----
+  GET /config/server/experiments/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "GerritBackendFeature__allow_fix_suggestions_in_comments": {
+      "enabled": false
+    },
+    "GerritBackendFeature__attach_nonce_to_documentation": {
+      "enabled": true
+    }
+  }
+----
+
+It is possible to specify the following options:
+
+[[list-experiments-enabled-only]]
+--
+* `enabled-only`: If specified only enabled experiments are listed.
+--
+
 [[list-tasks]]
 === List Tasks
 --
@@ -931,7 +974,7 @@
       "state": "SLEEPING",
       "start_time": "2014-06-11 12:58:51.508000000",
       "delay": 3287966,
-      "command": "Log File Compressor"
+      "command": "Log File Manager"
     }
   ]
 ----
@@ -1420,6 +1463,325 @@
 When `delete_missing` is set to `true` changes to be reindexed which are missing in NoteDb
 will be deleted in the index.
 
+[[list-indexes]]
+=== List Indexes
+--
+'GET /config/server/indexes'
+--
+
+Lists the indexes used by Gerrit. It provides details about the index versions,
+which index version is used to search and which versions are written to.
+
+This endpoint requires the
+link:access-control.html#capability_maintainServer[Maintain Server]
+capability.
+
+.Request
+----
+  GET /config/server/indexes/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "name": "accounts",
+      "versions": {
+        "13": {
+          "write": true,
+          "search": true
+        }
+      }
+    },
+    {
+      "name": "changes",
+      "versions": {
+        "83": {
+          "write": true,
+          "search": true
+        },
+        "84": {
+          "write": true,
+          "search": false
+        }
+      }
+    },
+    {
+      "name": "groups",
+      "versions": {
+        "10": {
+          "write": true,
+          "search": true
+        }
+      }
+    },
+    {
+      "name": "projects",
+      "versions": {
+        "8": {
+          "write": true,
+          "search": true
+        }
+      }
+    }
+  [
+----
+
+=== Get Index
+--
+'GET /config/server/indexes/changes'
+--
+
+Get an index used by Gerrit. It provides details about the index versions, which
+index version is used to search and which versions are written to.
+
+.Request
+----
+  'GET /config/server/indexes/changes'
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "name": "changes",
+    "versions": {
+      "83": {
+        "write": true,
+        "search": true
+      },
+      "84": {
+        "write": true,
+        "search": false
+      }
+    }
+  }
+----
+
+=== List Index Versions
+--
+'GET /config/server/indexes/changes/versions'
+--
+
+Lists versions of an index used by Gerrit.
+
+.Request
+----
+  'GET /config/server/indexes/changes/versions'
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "83": {
+      "write": true,
+      "search": true
+    },
+    "84": {
+      "write": true,
+      "search": false
+    }
+  }
+----
+
+=== Get Index Version
+--
+'GET /config/server/indexes/changes/versions/85'
+--
+
+Get info about one version of an index used by Gerrit.
+
+.Request
+----
+  'GET /config/server/indexes/changes/versions/84'
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "write": true,
+    "search": false
+  }
+----
+
+[[snapshot-index]]
+=== Create Index Snapshot
+
+These endpoints allow Gerrit admins to create index snapshots.
+Created snapshots can be used as a backup of the index.
+
+It is possible to create a snapshot of all indexes, snapshot of one index or
+snapshot of one index version.
+
+The snapshots will be stored on the server at `$SITE/index/snapshots/$ID`.
+The `$ID` can be optionally provided in link:#snapshot-index-input[SnapshotIndex.Input]
+or will default to the current local time in ISO8601 format.
+
+Only snapshots of indexes that Gerrit currently writes to can be created.
+
+Note, that the creation of multiple snapshots, e.g. of different index
+versions, is not atomic. If a consistent state over multiple indexes is
+required, the server has to be put into read-only mode before creating
+the snapshot.
+
+==== Create Snapshot of All Indexes
+--
+'POST /config/server/snapshot.indexes HTTP/1.0'
+--
+
+.Request
+----
+  POST /config/server/snapshot.indexes HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "id": "snapshot-1"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "snapshot-1"
+  }
+----
+
+==== Create Snapshot of one Index
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/snapshot'
+--
+
+This creates a snapshot of all write index versions of the specified index.
+
+.Request
+----
+  POST /config/server/indexes/accounts/snapshot HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "id": "snapshot-1"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "snapshot-1"
+  }
+----
+
+==== Create Snapshot of one Index Version
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/versions/#index-version[\{index-version\}]/snapshot'
+--
+
+This creates a snapshot of one index version of the specified index.
+
+.Request
+----
+  POST /config/server/indexes/changes/versions/84/snapshot HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "id": "snapshot-1"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "snapshot-1"
+  }
+----
+
+=== Reindex an Index Version
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/versions/#index-version[\{index-version\}]/reindex'
+--
+
+This endpoint allows to trigger background reindexing of an index version.  It is
+also supported to specify whether to reuse existing up-to-date (non-stale) index
+documents and whether to notifyListeners or not.
+
+.Request
+----
+  POST /config/server/indexes/changes/versions/84/reindex HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "reuse": "true",
+    "notifyListeners": "false"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 202 Accepted
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+----
+
+[[experiment-endpoints]]
+== Experiment Endpoints
+
+[[get-experiment]]
+=== Get Experiment
+--
+'GET /config/server/experiments/link:#experiment-name[\{experiment-name\}]
+--
+
+Retrieves the details of the experiment with the given name.
+
+Requires the caller to have the link:access-control.html#capability_administrateServer[
+Administrate Server] global capability.
+
+.Request
+----
+  GET /config/server/experiments/mGerritBackendFeature__attach_nonce_to_documentation HTTP/1.0
+----
+
+As response an link:#experiment-info[Experiment] entity is returned that
+describes the experiment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "enabled": true
+  }
+----
 
 [[ids]]
 == IDs
@@ -1434,10 +1796,21 @@
 Gerrit core caches can optionally be prefixed with "gerrit":
 "gerrit-<cache-name>".
 
+[[experiment-name]]
+=== \{experiment-name\}
+The name of the experiment.
+
 [[task-id]]
 === \{task-id\}
 The ID of the task (hex string).
 
+[[index-name]]
+=== \{index-name\}
+The name of the index. Can be any of: "accounts", "changes", "groups", "projects".
+
+[[index-version]]
+=== \{index-version\}
+The version of the index. This is an integer.
 
 [[json-entities]]
 == JSON Entities
@@ -1760,6 +2133,16 @@
 |`new_value`  |The new config value, picked up after reload.
 |======================
 
+[[experiment-info]]
+=== ExperimentInfo
+The `ExperimentInfo` entity contains information about an experiment.
+
+[options="header",cols="1,6"]
+|============================
+|Field Name |Description
+|`enabled`  |Whether the experiment is enabled.
+|============================
+
 [[download-info]]
 === DownloadInfo
 The `DownloadInfo` entity contains information about supported download
@@ -1787,6 +2170,9 @@
 |`url`               ||
 The URL of the download scheme, where '${project}' is used as
 placeholder for the project name.
+|`description`       |optional|
+An optional description of how the scheme works and maybe comparing
+it to other schemes, explaining the pros and cons of each option.
 |`is_auth_required`  |not set if `false`|
 Whether this download scheme requires authentication.
 |`is_auth_supported` |not set if `false`|
@@ -2067,6 +2453,19 @@
 requirements that are applicable for changes appearing in the dashboard.
 |=======================================
 
+[[snapshot-index-input]]
+=== SnapshotIndex.Input
+The `SnapshotIndex.Input` entity contains the parameters used to create an
+index snapshot.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name ||Description
+|`id` | optional |
+A string ID that will be used as the folder name containing the
+snapshots. Defaults to current timestamp.
+|=======================
+
 [[sshd-info]]
 === SshdInfo
 The `SshdInfo` entity contains information about Gerrit
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index fff9d0b..b0e1b49 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2467,6 +2467,100 @@
   ]
 ----
 
+DescendingOrder(d)::
+Sort the returned tags in descending order.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?d HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    },
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    }
+  ]
+----
+
+SortBy(sortby)::
+Sort the returned tags by one of the supported sort options: ref (default), creation_time.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?sortby=creation_time HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    }
+  ]
+----
+
 Substring(m)::
 Limit the results to those tags that match the specified substring.
 +
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0744ded..1095002 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -848,6 +848,7 @@
 The special "owner" parameter corresponds to the change owner.  Matches
 all changes that have a +2 vote from the change owner.
 
+[[non_uploader]]
 `label:Code-Review=+2,user=non_uploader`::
 `label:Code-Review=ok,user=non_uploader`::
 `label:Code-Review=+2,non_uploader`::
diff --git a/Documentation/user-suggest-edits.txt b/Documentation/user-suggest-edits.txt
index fa49eeb..55181a2 100644
--- a/Documentation/user-suggest-edits.txt
+++ b/Documentation/user-suggest-edits.txt
@@ -37,6 +37,21 @@
 Alternatively, you can use the copy to clipboard button to copy a suggested
 edit to your clipboard and then you can paste it into your editor.
 
+== Generate Suggestion
+
+Following UI needs to be activated by a plugin that implements SuggestionsProvider. Gerrit is providing just UI.
+
+** When a user types a comment, Gerrit queries a plugin for a code snippet. When there is a snippet, the user can see a preview of snippet under comment.
+
+image::images/generated-suggested-edit-preview.png["Generate Suggested Edit", align="center", width=400]
+
+** A user needs to click on "ADD SUGGESTION TO COMMENT" button if they want to use this suggestion. Otherwise the suggestion is never used.
+
+image::images/generated-suggested-edit-added.png["Added Generated Suggested Edit", align="center", width=400]
+
+** By clicking on "ADD SUGGESTION TO COMMENT" button, the suggestion is added to end of comment. The user can then edit the suggestion, if needed.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/MODULE.bazel b/MODULE.bazel
new file mode 100644
index 0000000..0b932b8
--- /dev/null
+++ b/MODULE.bazel
@@ -0,0 +1,2 @@
+# TODO(davido): Migrate all dependencies from WORKSPACE to MODULE.bazel
+# https://issues.gerritcodereview.com/issues/303819949
diff --git a/WORKSPACE b/WORKSPACE
index 8ce21d5..1c168c6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -26,47 +26,22 @@
 
 load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
-load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "maven_jar")
 load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
 load("//tools:nongoogle.bzl", "declare_nongoogle_deps")
 load("//tools:deps.bzl", "CAFFEINE_VERS", "java_dependencies")
 
 http_archive(
-    name = "platforms",
-    sha256 = "3a561c99e7bdbe9173aa653fd579fe849f1d8d67395780ab4770b1f381431d51",
-    urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
-        "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
-    ],
+    name = "rules_nodejs",
+    patch_args = ["-p1"],
+    patches = ["//tools:rules_nodejs-5.8.4-node_versions.bzl.patch"],
+    sha256 = "8fc8e300cb67b89ceebd5b8ba6896ff273c84f6099fc88d23f24e7102319d8fd",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.4/rules_nodejs-core-5.8.4.tar.gz"],
 )
 
 http_archive(
-    name = "rbe_jdk11",
-    sha256 = "dbcfd6f26589ef506b91fe03a12dc559ca9c84699e4cf6381150522287f0e6f6",
-    strip_prefix = "rbe_autoconfig-3.1.0",
-    urls = [
-        "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v3.1.0.tar.gz",
-        "https://github.com/davido/rbe_autoconfig/archive/v3.1.0.tar.gz",
-    ],
-)
-
-http_archive(
-    name = "com_google_protobuf",
-    sha256 = "3bd7828aa5af4b13b99c191e8b1e884ebfa9ad371b0ce264605d347f135d2568",
-    strip_prefix = "protobuf-3.19.4",
-    urls = [
-        "https://github.com/protocolbuffers/protobuf/archive/v3.19.4.tar.gz",
-    ],
-)
-
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
-
-protobuf_deps()
-
-http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "94070eff79305be05b7699207fbac5d2608054dd53e6109f7d00d923919ff45a",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.2/rules_nodejs-5.8.2.tar.gz"],
+    sha256 = "709cc0dcb51cf9028dd57c268066e5bc8f03a119ded410a13b5c3925d6e43c48",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.4/rules_nodejs-5.8.4.tar.gz"],
 )
 
 load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -99,9 +74,11 @@
     firefox = True,
 )
 
-register_toolchains("//tools:error_prone_warnings_toolchain_java11_definition")
+declare_nongoogle_deps()
 
-register_toolchains("//tools:error_prone_warnings_toolchain_java17_definition")
+load("//tools:defs.bzl", "gerrit_init")
+
+gerrit_init()
 
 # Java-Prettify external repository consumed from git submodule
 local_repository(
@@ -137,12 +114,10 @@
     ],
 )
 
-declare_nongoogle_deps()
-
 load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
 
 node_repositories(
-    node_version = "17.9.1",
+    node_version = "20.9.0",
     yarn_version = "1.22.19",
 )
 
diff --git a/contrib/bug-icon/1_bug_icon_16x16.ai b/contrib/bug-icon/1_bug_icon_16x16.ai
new file mode 100644
index 0000000..5872a27
--- /dev/null
+++ b/contrib/bug-icon/1_bug_icon_16x16.ai
Binary files differ
diff --git a/contrib/bug-icon/1_bug_icon_16x16.svg b/contrib/bug-icon/1_bug_icon_16x16.svg
new file mode 100644
index 0000000..ff4db74
--- /dev/null
+++ b/contrib/bug-icon/1_bug_icon_16x16.svg
@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   viewBox="0 0 21.333332 21.333332"
+   height="21.333332"
+   width="21.333332"
+   xml:space="preserve"
+   id="svg2"
+   version="1.1"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+     id="defs6"><clipPath
+       id="clipPath18"
+       clipPathUnits="userSpaceOnUse"><path
+         id="path16"
+         d="M 0,0 H 16 V 16 H 0 Z" /></clipPath></defs><g
+     transform="matrix(1.3333333,0,0,-1.3333333,0,21.333333)"
+     id="g10"><g
+       id="g12"><g
+         clip-path="url(#clipPath18)"
+         id="g14"><g
+           transform="translate(4.022,5.7807)"
+           id="g20"><path
+             id="path22"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C -0.005,-0.002 -0.011,-0.003 -0.017,-0.002 L -0.755,0.109 C -0.771,0.111 -0.783,0.124 -0.784,0.14 l -0.047,0.484 h -0.263 c -0.019,0 -0.034,0.016 -0.034,0.035 0,0.019 0.015,0.034 0.034,0.034 H -0.8 c 0.018,0 0.033,-0.014 0.035,-0.032 L -0.718,0.173 -0.007,0.066 C 0.012,0.063 0.025,0.046 0.022,0.027 0.02,0.014 0.011,0.004 0,0" /></g><g
+           transform="translate(3.3183,4.6576)"
+           id="g24"><path
+             id="path26"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.002,-0.001 -0.004,-0.001 -0.007,-0.002 l -0.138,-0.023 c -0.018,-0.003 -0.036,0.009 -0.039,0.028 -0.004,0.019 0.009,0.037 0.028,0.04 l 0.115,0.019 0.11,0.55 c 0.003,0.014 0.014,0.025 0.028,0.027 l 0.57,0.097 C 0.686,0.74 0.704,0.727 0.707,0.708 0.71,0.69 0.698,0.672 0.679,0.669 L 0.132,0.575 0.021,0.025 C 0.019,0.014 0.011,0.004 0,0" /></g><g
+           transform="translate(3.9726,3.8581)"
+           id="g28"><path
+             id="path30"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.014,-0.007 -0.031,-0.004 -0.042,0.008 -0.012,0.014 -0.011,0.036 0.003,0.049 l 0.088,0.076 -0.192,0.526 c -0.005,0.014 -0.001,0.029 0.01,0.038 L 0.303,1.078 C 0.317,1.09 0.339,1.089 0.351,1.074 0.364,1.06 0.362,1.038 0.348,1.026 L -0.07,0.661 0.122,0.134 C 0.127,0.121 0.123,0.106 0.112,0.096 L 0.007,0.005 C 0.005,0.003 0.002,0.001 0,0" /></g><g
+           transform="translate(5.0103,6.1574)"
+           id="g32"><path
+             id="path34"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.005,0.002 0.01,0.005 0.014,0.01 l 0.477,0.574 c 0.01,0.012 0.011,0.03 0.002,0.042 L 0.205,1.019 0.401,1.194 C 0.415,1.207 0.416,1.228 0.404,1.243 0.391,1.257 0.369,1.258 0.355,1.245 L 0.136,1.049 C 0.122,1.037 0.12,1.017 0.131,1.003 L 0.421,0.607 -0.039,0.054 C -0.051,0.04 -0.049,0.018 -0.034,0.006 -0.024,-0.003 -0.011,-0.004 0,0" /></g><g
+           transform="translate(6.3269,4.7555)"
+           id="g36"><path
+             id="path38"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.015,0.004 0.026,0.018 0.026,0.034 0,0.019 -0.016,0.034 -0.035,0.034 L -0.125,0.066 -0.332,0.587 C -0.337,0.601 -0.35,0.609 -0.364,0.609 L -0.943,0.603 C -0.962,0.602 -0.977,0.587 -0.977,0.568 c 0,-0.019 0.016,-0.034 0.035,-0.034 L -0.387,0.54 -0.18,0.019 c 0.005,-0.013 0.018,-0.022 0.032,-0.022 l 0.14,0.002 c 0.003,0 0.005,0 0.008,0.001" /></g><g
+           transform="translate(4.0513,6.5954)"
+           id="g40"><path
+             id="path42"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C -0.008,-0.003 -0.017,-0.003 -0.025,0 L -0.328,0.12 C -0.342,0.125 -0.351,0.138 -0.35,0.152 l 0.004,0.237 c 0,0.019 0.016,0.034 0.035,0.034 0.019,0 0.034,-0.016 0.034,-0.035 L -0.281,0.175 0,0.064 C 0.018,0.057 0.027,0.037 0.02,0.02 0.016,0.01 0.009,0.003 0,0" /></g><g
+           transform="translate(4.4462,6.7459)"
+           id="g44"><path
+             id="path46"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.008,0.003 0.014,0.009 0.018,0.017 L 0.166,0.308 C 0.172,0.321 0.17,0.336 0.16,0.347 L -0.001,0.521 C -0.014,0.535 -0.036,0.535 -0.05,0.523 -0.064,0.51 -0.064,0.488 -0.051,0.474 L 0.093,0.318 -0.043,0.048 C -0.052,0.031 -0.045,0.01 -0.028,0.001 -0.019,-0.003 -0.009,-0.003 0,0" /></g><g
+           transform="translate(4.6939,6.6224)"
+           id="g48"><path
+             id="path50"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.078,-0.206 -0.025,-0.436 -0.231,-0.515 -0.205,-0.078 -0.436,0.025 -0.514,0.231 -0.079,0.206 0.025,0.436 0.23,0.515 C -0.309,0.309 -0.078,0.206 0,0" /></g><g
+           transform="translate(5.3048,5.0195)"
+           id="g52"><path
+             id="path54"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.081,-0.117 -0.192,-0.208 -0.328,-0.26 -0.136,-0.051 -0.279,-0.057 -0.417,-0.024 0,0 -0.308,0.808 0.064,0.95 C -0.308,0.808 0,0 0,0" /></g><g
+           transform="translate(5.3871,5.9087)"
+           id="g56"><path
+             id="path58"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.104,-0.274 0.089,-0.559 -0.02,-0.782 -0.018,-0.038 -0.039,-0.073 -0.062,-0.107 -0.308,0.016 -0.545,0.31 -0.545,0.31 0,0 0.014,-0.379 -0.2,-0.594 -0.04,0.009 -0.08,0.022 -0.118,0.038 -0.23,0.094 -0.431,0.297 -0.536,0.571 -0.182,0.478 0.037,0.899 0.446,1.054 C -0.626,0.646 -0.182,0.479 0,0" /></g><g
+           transform="translate(5.2032,5.8241)"
+           id="g60"><path
+             id="path62"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.116,-0.305 -0.037,-0.646 -0.341,-0.762 -0.305,-0.116 -0.646,0.037 -0.762,0.342 -0.116,0.304 0.037,0.645 0.341,0.761 C -0.457,0.457 -0.116,0.305 0,0" /></g><g
+           transform="translate(4.5752,6.005)"
+           id="g64"><path
+             id="path66"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 0.164,-0.509 0.09,-0.538 -0.127,-0.048 Z" /></g><g
+           transform="translate(4.8228,5.3149)"
+           id="g68"><path
+             id="path70"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -0.1,-0.038 -0.038,0.1 0.1,0.038 z" /></g><g
+           transform="translate(6.9544,3.7147)"
+           id="g72"><path
+             id="path74"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.004,-0.002 -0.009,-0.004 -0.015,-0.004 l -0.639,0.012 c -0.014,0 -0.026,0.01 -0.029,0.023 L -0.776,0.437 -1,0.408 c -0.016,-0.002 -0.031,0.009 -0.033,0.026 -0.002,0.016 0.009,0.031 0.026,0.033 l 0.25,0.032 c 0.015,0.002 0.029,-0.008 0.032,-0.022 L -0.63,0.067 -0.013,0.056 C 0.003,0.055 0.016,0.042 0.016,0.025 0.015,0.014 0.009,0.005 0,0" /></g><g
+           transform="translate(6.4819,2.6811)"
+           id="g76"><path
+             id="path78"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C -0.002,-0.001 -0.003,-0.002 -0.005,-0.002 L -0.12,-0.037 c -0.016,-0.005 -0.032,0.003 -0.037,0.019 -0.005,0.016 0.004,0.032 0.019,0.037 l 0.096,0.029 0.032,0.48 c 0.001,0.012 0.009,0.023 0.021,0.026 L 0.485,0.701 C 0.5,0.706 0.517,0.697 0.522,0.681 0.526,0.666 0.518,0.649 0.502,0.644 L 0.047,0.504 0.015,0.024 C 0.015,0.014 0.009,0.005 0,0" /></g><g
+           transform="translate(7.1277,2.0748)"
+           id="g80"><path
+             id="path82"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C -0.011,-0.008 -0.026,-0.007 -0.037,0.002 -0.049,0.013 -0.05,0.032 -0.039,0.044 L 0.027,0.118 -0.196,0.544 C -0.201,0.555 -0.2,0.569 -0.191,0.578 L 0.137,0.95 C 0.147,0.962 0.166,0.963 0.178,0.953 0.191,0.942 0.192,0.923 0.181,0.911 L -0.134,0.554 0.089,0.128 C 0.094,0.117 0.093,0.104 0.085,0.094 L 0.005,0.005 C 0.004,0.003 0.002,0.001 0,0" /></g><g
+           transform="translate(7.7523,4.1457)"
+           id="g84"><path
+             id="path86"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.004,0.002 0.008,0.006 0.011,0.01 L 0.352,0.551 C 0.359,0.563 0.358,0.578 0.349,0.588 L 0.06,0.889 0.207,1.06 C 0.218,1.072 0.216,1.091 0.204,1.102 0.191,1.112 0.173,1.111 0.162,1.098 L -0.002,0.907 c -0.01,-0.012 -0.01,-0.029 0.001,-0.04 L 0.29,0.563 -0.039,0.042 C -0.048,0.028 -0.044,0.01 -0.03,0.001 -0.02,-0.005 -0.009,-0.005 0,0" /></g><g
+           transform="translate(9.0287,3.1014)"
+           id="g88"><path
+             id="path90"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.012,0.005 0.02,0.018 0.018,0.032 0.016,0.048 0.001,0.059 -0.015,0.057 l -0.099,-0.015 -0.234,0.42 c -0.006,0.011 -0.018,0.017 -0.03,0.015 L -0.869,0.407 C -0.885,0.404 -0.897,0.389 -0.894,0.373 -0.892,0.357 -0.877,0.346 -0.861,0.348 l 0.471,0.068 0.234,-0.42 c 0.006,-0.011 0.018,-0.017 0.031,-0.015 l 0.118,0.017 C -0.004,-0.001 -0.002,-0.001 0,0" /></g><g
+           transform="translate(6.888,4.4106)"
+           id="g92"><path
+             id="path94"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.006,-0.003 -0.014,-0.005 -0.021,-0.003 l -0.272,0.068 c -0.012,0.003 -0.02,0.013 -0.022,0.025 l -0.023,0.202 c -0.001,0.016 0.01,0.031 0.026,0.033 0.017,0.002 0.031,-0.01 0.033,-0.026 L -0.258,0.117 -0.007,0.055 C 0.009,0.051 0.019,0.035 0.015,0.019 0.013,0.01 0.007,0.004 0,0" /></g><g
+           transform="translate(7.2069,4.5828)"
+           id="g96"><path
+             id="path98"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.006,0.003 0.011,0.009 0.014,0.016 L 0.106,0.28 C 0.11,0.292 0.107,0.305 0.097,0.313 l -0.156,0.13 C -0.072,0.453 -0.09,0.451 -0.101,0.439 -0.111,0.426 -0.109,0.407 -0.097,0.397 L 0.044,0.28 -0.042,0.036 C -0.047,0.02 -0.039,0.003 -0.024,-0.002 -0.016,-0.005 -0.007,-0.004 0,0" /></g><g
+           transform="translate(7.4312,4.5055)"
+           id="g100"><path
+             id="path102"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.09,-0.166 0.028,-0.374 -0.138,-0.463 -0.167,-0.09 -0.374,-0.028 -0.464,0.138 -0.089,0.166 -0.028,0.374 0.139,0.463 C -0.297,0.228 -0.09,0.166 0,0" /></g><g
+           transform="translate(8.1302,3.2114)"
+           id="g104"><path
+             id="path106"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.056,-0.108 -0.14,-0.198 -0.25,-0.257 -0.109,-0.06 -0.231,-0.081 -0.352,-0.068 0,0 -0.352,0.652 -0.051,0.815 C -0.352,0.652 0,0 0,0" /></g><g
+           transform="translate(8.1005,3.9765)"
+           id="g108"><path
+             id="path110"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.119,-0.221 0.138,-0.465 0.071,-0.667 0.06,-0.701 0.046,-0.734 0.03,-0.765 c -0.263,-0.021 -0.499,0.203 -0.499,0.203 0,0 0.055,-0.321 -0.103,-0.528 -0.035,0.004 -0.07,0.01 -0.105,0.019 -0.205,0.055 -0.399,0.204 -0.519,0.425 -0.208,0.387 -0.069,0.769 0.261,0.947 C -0.605,0.479 -0.209,0.386 0,0" /></g><g
+           transform="translate(7.9536,3.8839)"
+           id="g112"><path
+             id="path114"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.133,-0.246 0.041,-0.553 -0.205,-0.686 -0.246,-0.133 -0.553,-0.041 -0.686,0.205 -0.133,0.246 -0.041,0.553 0.205,0.686 C -0.44,0.338 -0.133,0.246 0,0" /></g><g
+           transform="translate(7.3995,3.9674)"
+           id="g116"><path
+             id="path118"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 0.197,-0.415 0.136,-0.447 -0.102,-0.055 Z" /></g><g
+           transform="translate(7.6873,3.4084)"
+           id="g120"><path
+             id="path122"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -0.081,-0.044 -0.043,0.081 0.08,0.044 z" /></g><g
+           transform="translate(4.2747,2.386)"
+           id="g124"><path
+             id="path126"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.004,-0.002 -0.008,-0.003 -0.013,-0.003 l -0.573,0.01 c -0.012,0 -0.023,0.009 -0.025,0.021 l -0.084,0.364 -0.2,-0.026 c -0.015,-0.002 -0.028,0.008 -0.03,0.022 -0.002,0.015 0.008,0.028 0.023,0.03 l 0.224,0.029 c 0.013,0.002 0.026,-0.007 0.029,-0.02 L -0.564,0.06 -0.012,0.05 C 0.003,0.049 0.014,0.037 0.014,0.023 0.014,0.013 0.008,0.004 0,0" /></g><g
+           transform="translate(3.8515,1.4604)"
+           id="g128"><path
+             id="path130"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.002,-0.001 -0.003,-0.001 -0.005,-0.002 l -0.103,-0.032 c -0.014,-0.004 -0.028,0.004 -0.033,0.018 -0.004,0.014 0.004,0.029 0.018,0.033 l 0.085,0.026 0.029,0.43 c 10e-4,0.011 0.008,0.02 0.019,0.023 L 0.434,0.628 C 0.448,0.632 0.463,0.624 0.467,0.61 0.471,0.596 0.464,0.581 0.45,0.577 L 0.043,0.451 0.014,0.021 C 0.013,0.012 0.008,0.004 0,0" /></g><g
+           transform="translate(4.4299,0.9173)"
+           id="g132"><path
+             id="path134"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.01,-0.007 -0.023,-0.006 -0.033,0.002 -0.011,0.01 -0.012,0.026 -0.002,0.037 L 0.024,0.106 -0.175,0.488 C -0.18,0.497 -0.179,0.509 -0.172,0.517 L 0.122,0.851 C 0.132,0.862 0.149,0.863 0.16,0.853 0.171,0.843 0.172,0.827 0.162,0.816 L -0.12,0.496 0.08,0.114 C 0.085,0.105 0.083,0.093 0.076,0.085 L 0.005,0.004 C 0.003,0.003 0.002,0.001 0,0" /></g><g
+           transform="translate(4.9893,2.772)"
+           id="g136"><path
+             id="path138"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.004,0.002 0.007,0.005 0.01,0.009 L 0.315,0.494 C 0.322,0.504 0.321,0.518 0.312,0.526 L 0.054,0.796 0.185,0.949 C 0.195,0.96 0.194,0.977 0.183,0.987 0.171,0.996 0.155,0.995 0.145,0.984 L -0.002,0.812 C -0.011,0.802 -0.01,0.787 -0.001,0.777 L 0.26,0.505 -0.035,0.037 C -0.043,0.025 -0.039,0.009 -0.027,0.001 -0.018,-0.004 -0.008,-0.004 0,0" /></g><g
+           transform="translate(6.1324,1.8367)"
+           id="g140"><path
+             id="path142"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.011,0.004 0.018,0.016 0.016,0.028 0.014,0.043 0.001,0.053 -0.014,0.051 L -0.102,0.038 -0.312,0.414 C -0.317,0.423 -0.328,0.429 -0.339,0.427 L -0.779,0.364 C -0.793,0.362 -0.803,0.349 -0.801,0.334 -0.799,0.32 -0.786,0.31 -0.771,0.312 l 0.422,0.06 0.21,-0.376 c 0.005,-0.009 0.016,-0.015 0.027,-0.013 l 0.106,0.015 C -0.004,-0.001 -0.002,-0.001 0,0" /></g><g
+           transform="translate(4.2152,3.0093)"
+           id="g144"><path
+             id="path146"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.006,-0.003 -0.012,-0.004 -0.019,-0.002 l -0.243,0.06 c -0.011,0.003 -0.019,0.012 -0.02,0.023 l -0.02,0.181 c -0.002,0.014 0.008,0.027 0.023,0.029 0.014,0.001 0.028,-0.009 0.029,-0.023 L -0.231,0.105 -0.006,0.049 C 0.008,0.045 0.017,0.031 0.013,0.017 0.011,0.009 0.006,0.003 0,0" /></g><g
+           transform="translate(4.5008,3.1635)"
+           id="g148"><path
+             id="path150"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0.006,0.003 0.01,0.008 0.012,0.015 L 0.095,0.251 C 0.099,0.261 0.096,0.273 0.087,0.28 l -0.14,0.116 C -0.064,0.406 -0.081,0.404 -0.09,0.393 -0.1,0.382 -0.098,0.365 -0.087,0.356 L 0.039,0.251 -0.038,0.032 C -0.042,0.018 -0.035,0.003 -0.021,-0.002 -0.014,-0.004 -0.006,-0.003 0,0" /></g><g
+           transform="translate(4.7017,3.0943)"
+           id="g152"><path
+             id="path154"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.08,-0.149 0.025,-0.335 -0.124,-0.415 -0.149,-0.08 -0.335,-0.025 -0.415,0.124 -0.08,0.149 -0.025,0.335 0.124,0.415 C -0.266,0.204 -0.08,0.149 0,0" /></g><g
+           transform="translate(5.3277,1.9352)"
+           id="g156"><path
+             id="path158"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.05,-0.097 -0.125,-0.178 -0.224,-0.231 -0.098,-0.053 -0.207,-0.071 -0.315,-0.06 0,0 -0.315,0.584 -0.046,0.73 C -0.315,0.584 0,0 0,0" /></g><g
+           transform="translate(5.3011,2.6205)"
+           id="g160"><path
+             id="path162"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.107,-0.198 0.124,-0.417 0.064,-0.597 -0.011,-0.031 -0.023,-0.06 -0.037,-0.088 -0.236,-0.019 -0.447,0.182 -0.447,0.182 0,0 0.049,-0.288 -0.092,-0.473 -0.032,0.003 -0.063,0.009 -0.094,0.017 -0.184,0.049 -0.358,0.183 -0.465,0.381 C -1.258,-0.232 -1.133,0.11 -0.837,0.27 -0.541,0.429 -0.187,0.346 0,0" /></g><g
+           transform="translate(5.1696,2.5376)"
+           id="g164"><path
+             id="path166"
+             style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0.119,-0.22 0.037,-0.495 -0.183,-0.614 -0.221,-0.119 -0.496,-0.037 -0.615,0.183 -0.119,0.22 -0.037,0.495 0.184,0.614 C -0.394,0.302 -0.119,0.22 0,0" /></g><g
+           transform="translate(4.6733,2.6123)"
+           id="g168"><path
+             id="path170"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 0.176,-0.371 0.122,-0.4 -0.092,-0.05 Z" /></g><g
+           transform="translate(4.931,2.1117)"
+           id="g172"><path
+             id="path174"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -0.072,-0.039 -0.039,0.072 0.072,0.039 z" /></g><g
+           transform="translate(10.5983,13.5956)"
+           id="g176"><path
+             id="path178"
+             style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.1,-0.21 -0.136,-0.276 -0.236,-0.234 l -0.007,0.003 h -0.008 c -0.019,-0.001 -0.037,-0.001 -0.056,-0.003 -0.035,0.005 -0.093,0.012 -0.143,0.015 0.021,0.045 0.056,0.106 0.109,0.185 C -0.12,0.303 0.183,0.505 0.365,0.601 0.176,0.369 0.074,0.155 0,0 m 17.206,-10.696 -0.021,0.016 c -0.033,0.022 -3.325,2.248 -4.636,3.731 -0.173,0.196 -0.35,0.422 -0.527,0.666 10e-4,0.017 0,0.034 0,0.052 v 0.015 l -0.01,0.011 c -0.04,0.037 -0.082,0.075 -0.125,0.113 -0.187,0.269 -0.373,0.555 -0.552,0.845 0.226,-0.145 0.453,-0.316 0.684,-0.526 l 0.065,-0.06 -0.006,0.089 c -10e-4,0.007 -0.059,0.777 -0.104,1.107 l -10e-4,0.009 -0.005,0.007 c -0.134,0.191 -0.994,0.713 -1.601,1.067 -0.033,0.062 -0.066,0.123 -0.096,0.182 0.53,-0.149 1.007,-0.44 1.61,-0.873 l 0.068,-0.049 -0.012,0.083 c -10e-4,0.008 -0.117,0.767 -0.219,1.153 l -0.002,0.007 -0.005,0.007 c -0.118,0.148 -0.88,0.528 -1.278,0.719 0.372,-0.074 0.638,-0.171 1.144,-0.396 l 0.075,-0.034 -0.028,0.079 C 11.616,-2.656 10.843,-0.633 7.987,1.04 7.601,1.266 7.227,1.435 6.869,1.56 v 0 C 6.727,1.715 6.469,1.829 6.074,1.919 5.843,1.972 5.614,2.012 5.389,2.04 5.429,2.104 5.469,2.165 5.502,2.207 5.728,2.504 6.094,3.175 6.109,3.203 L 6.128,3.237 4.605,3.852 4.593,3.815 C 4.592,3.81 4.438,3.291 4.077,2.877 3.738,2.489 3.406,2.084 3.355,2.022 2.915,1.955 2.509,1.851 2.144,1.722 1.712,1.679 0.864,1.564 0.218,1.284 -0.128,1.134 -0.458,0.804 -0.627,0.444 -0.709,0.267 -0.727,-0.033 -0.726,-0.261 -0.77,-0.272 -0.808,-0.283 -0.828,-0.289 -1.56,-0.41 -2.248,-0.714 -2.741,-1.146 -3.037,-1.404 -3.291,-1.735 -3.486,-2.105 -3.506,-2.146 -3.527,-2.186 -3.547,-2.228 -3.576,-2.257 -3.591,-2.275 -3.593,-2.276 L -3.597,-2.28 -3.6,-2.286 c -0.005,-0.012 -0.133,-0.315 -0.178,-0.477 -0.05,-0.178 -0.104,-0.535 -0.106,-0.549 l -0.014,-0.101 0.012,0.012 c -0.006,-0.126 0.004,-0.268 0.025,-0.416 -0.31,-0.111 -0.613,-0.263 -0.757,-0.452 -0.618,-0.738 -0.567,-1.514 -0.517,-1.813 0.052,-0.311 0.162,-0.522 0.243,-0.571 0.064,-0.038 0.118,-0.046 0.17,-0.037 0.081,0.014 0.156,0.075 0.241,0.145 0.04,0.035 0.086,0.073 0.139,0.11 0.037,0.027 0.082,0.061 0.135,0.101 0.271,0.205 0.725,0.547 1.168,0.716 0.046,0.018 0.09,0.034 0.131,0.048 0.243,-0.141 0.55,-0.262 0.909,-0.291 0.125,-0.01 0.247,-0.017 0.362,-0.023 l -0.017,-0.009 0.123,-0.003 c 0.55,-0.014 0.945,0.019 1.257,0.071 0.051,0.009 0.098,0.017 0.145,0.026 0.03,0.006 0.061,0.014 0.091,0.02 0.008,0.001 0.016,0.003 0.024,0.006 0.018,0.003 0.035,0.008 0.053,0.012 0.052,0.012 0.102,0.025 0.151,0.038 0.016,0.005 0.032,0.009 0.048,0.014 0.06,0.016 0.118,0.033 0.174,0.048 0.271,0.078 0.527,0.153 0.893,0.176 0.725,0.046 1.616,0.064 2.362,0.038 L 3.436,-6.039 C 3.313,-6.045 2.764,-6.106 2.132,-6.639 1.433,-7.228 1.085,-7.946 1.081,-7.953 l -0.04,-0.085 0.351,0.151 0.007,0.008 c 0.005,0.006 0.536,0.698 0.884,0.904 0.196,0.115 0.409,0.208 0.559,0.268 C 2.61,-6.895 2.21,-7.219 2.103,-7.314 1.921,-7.475 1.696,-7.776 1.687,-7.789 l -0.069,-0.091 0.464,0.159 0.006,0.004 c 0.534,0.444 1.226,0.973 1.845,1.133 0.519,0.121 1.29,-0.201 1.297,-0.205 l 0.041,0.055 -0.239,0.303 -0.01,0.004 C 5.017,-6.425 4.571,-6.265 4.33,-6.138 4.312,-6.129 4.3,-6.113 4.293,-6.089 4.261,-5.971 4.356,-5.724 4.464,-5.502 4.568,-5.513 4.665,-5.526 4.75,-5.541 5.088,-5.601 5.591,-5.62 6.044,-5.623 6.061,-5.628 6.142,-5.652 6.269,-5.689 L 6.153,-6.538 C 6.038,-6.56 5.613,-7.448 6.137,-7.23 c 0.063,0.027 0.117,0.051 0.165,0.073 0.222,0.101 0.334,0.154 0.977,0.172 0.359,0.01 0.608,-0.037 0.61,-0.037 l 0.12,-0.023 -0.267,0.249 -0.006,0.003 c -0.009,0.005 -0.232,0.098 -0.351,0.136 -0.121,0.04 -0.34,0.093 -0.423,0.111 l 0.102,0.628 c 0.681,-0.196 1.586,-0.45 2.336,-0.641 0.204,-0.053 0.407,-0.099 0.604,-0.14 0.431,-0.77 0.909,-1.561 1.356,-2.179 1.274,-1.753 4.515,-5.216 4.548,-5.25 l 0.036,-0.039 0.642,1.439 -2.529,2.712 2.626,-2.231 z" /></g><g
+           transform="translate(8.3941,8.2561)"
+           id="g180"><path
+             id="path182"
+             style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.032,-0.099 -0.122,-0.115 -0.269,-0.142 -0.13,-0.023 -0.306,-0.055 -0.558,-0.152 -0.449,-0.172 -0.91,-0.519 -1.185,-0.727 -0.053,-0.04 -0.099,-0.075 -0.137,-0.102 -0.053,-0.038 -0.1,-0.077 -0.141,-0.111 -0.087,-0.072 -0.162,-0.134 -0.244,-0.148 -0.053,-0.01 -0.109,0 -0.173,0.038 -0.083,0.05 -0.194,0.264 -0.247,0.579 -0.051,0.303 -0.102,1.091 0.525,1.839 0.331,0.433 1.467,0.677 1.712,0.673 0.219,-0.005 0.352,-0.334 0.48,-0.653 C -0.216,1.041 -0.195,0.988 -0.173,0.938 -0.024,0.584 0.053,0.163 0,0" /></g><g
+           transform="translate(3.4987,9.1203)"
+           id="g184"><path
+             id="path186"
+             style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c -0.143,-0.113 -0.35,-0.165 -0.59,-0.158 -0.037,0.001 -0.075,0.004 -0.114,0.008 -0.252,0.026 -0.531,0.113 -0.802,0.258 -0.026,0.014 -0.053,0.029 -0.079,0.044 -0.044,0.026 -0.087,0.052 -0.13,0.081 -0.221,0.146 -0.409,0.318 -0.555,0.498 -0.025,0.03 -0.048,0.06 -0.07,0.091 -0.027,0.036 -0.051,0.072 -0.074,0.109 -0.024,0.038 -0.046,0.077 -0.067,0.115 -0.094,0.18 -0.144,0.357 -0.141,0.516 0,0.032 0.003,0.062 0.007,0.092 0.012,0.079 0.04,0.152 0.082,0.217 0.09,0.135 0.238,0.22 0.423,0.258 0.049,0.01 0.1,0.017 0.153,0.02 0.036,0.002 0.073,0.003 0.11,0.002 C -1.553,2.145 -1.209,2.049 -0.879,1.865 -0.853,1.851 -0.827,1.836 -0.801,1.82 -0.769,1.801 -0.737,1.781 -0.705,1.76 -0.403,1.56 -0.161,1.311 -0.004,1.058 0.017,1.025 0.036,0.992 0.054,0.959 V 0.958 C 0.221,0.646 0.255,0.338 0.112,0.121 0.1,0.102 0.086,0.085 0.071,0.068 0.05,0.043 0.026,0.02 0,0 M 7.404,-2.984 7.374,-2.97 0.53,0.207 C 0.672,0.788 0.267,1.544 -0.508,2.057 -0.928,2.335 -1.394,2.495 -1.818,2.508 -2.274,2.521 -2.633,2.365 -2.83,2.068 -2.923,1.927 -2.972,1.766 -2.979,1.596 -2.982,1.539 -2.98,1.482 -2.973,1.423 -2.951,1.217 -2.874,1 -2.749,0.788 -2.733,0.761 -2.716,0.734 -2.698,0.707 -2.651,0.635 -2.599,0.563 -2.541,0.493 -2.51,0.457 -2.478,0.42 -2.445,0.384 c 0.15,-0.161 0.329,-0.313 0.533,-0.448 0.062,-0.042 0.126,-0.08 0.19,-0.116 0.026,-0.015 0.053,-0.029 0.079,-0.043 0.214,-0.112 0.434,-0.194 0.651,-0.242 0.035,-0.008 0.07,-0.015 0.105,-0.021 0.096,-0.016 0.192,-0.026 0.285,-0.029 0.031,-0.001 0.061,-0.001 0.091,0 0.032,0 0.063,0.002 0.094,0.004 0.062,0.005 0.121,0.013 0.178,0.025 0.087,0.017 0.167,0.043 0.242,0.075 0.056,0.025 0.109,0.054 0.158,0.088 0.085,0.057 0.158,0.126 0.218,0.207 l 6.83,-3.17 c 0,-10e-4 0,-10e-4 0,-10e-4 l 0.056,-0.026 c 0.015,-0.003 0.029,-0.006 0.045,-0.006 0.099,-10e-4 0.181,0.078 0.182,0.178 0.001,0.067 -0.035,0.126 -0.088,0.157" /></g><g
+           transform="translate(3.8027,9.3432)"
+           id="g188"><path
+             id="path190"
+             style="fill:#455a64;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -0.01,-0.035 0.003,-0.002 c -0.026,-0.102 -0.085,-0.236 -0.162,-0.376 -0.017,0.087 -0.038,0.173 -0.064,0.258 -0.05,0.165 -0.118,0.323 -0.203,0.473 0.07,0.135 0.132,0.275 0.186,0.418 v 0 c 0.021,0.055 0.04,0.11 0.059,0.166 C 0.002,0.59 0.079,0.267 0,0 m -0.623,-1.053 c -0.025,0.109 -0.057,0.216 -0.098,0.319 -0.047,0.122 -0.106,0.24 -0.175,0.351 0.001,0.001 0.002,0.001 0.002,0.002 0.155,0.189 0.291,0.392 0.409,0.606 0.076,-0.142 0.137,-0.293 0.181,-0.448 0.029,-0.101 0.051,-0.205 0.066,-0.31 C -0.259,-0.566 -0.28,-0.6 -0.301,-0.634 -0.405,-0.794 -0.52,-0.945 -0.623,-1.053 m -0.138,-0.119 c -0.044,-0.029 -0.09,-0.058 -0.135,-0.086 -0.092,-0.058 -0.184,-0.116 -0.272,-0.18 -0.057,0.178 -0.141,0.348 -0.253,0.499 0.045,0.046 0.09,0.091 0.135,0.135 0.031,0.032 0.063,0.063 0.095,0.095 0.046,0.047 0.093,0.095 0.137,0.143 0.034,0.036 0.066,0.073 0.098,0.11 0.054,-0.09 0.101,-0.185 0.141,-0.282 0.051,-0.125 0.089,-0.255 0.116,-0.387 -0.022,-0.018 -0.042,-0.035 -0.062,-0.047 m -0.745,-0.612 c -0.026,-0.042 -0.047,-0.087 -0.067,-0.132 -0.077,0.147 -0.172,0.283 -0.287,0.403 0.03,0.056 0.061,0.111 0.096,0.164 0.081,0.123 0.179,0.236 0.281,0.344 0.108,-0.149 0.189,-0.315 0.24,-0.491 -0.101,-0.082 -0.193,-0.175 -0.263,-0.288 m -0.145,-0.331 c -0.035,-0.097 -0.071,-0.196 -0.12,-0.285 -0.06,-0.107 -0.144,-0.198 -0.241,-0.27 -0.042,0.136 -0.103,0.266 -0.185,0.382 0.047,0.102 0.09,0.207 0.131,0.309 0.051,0.127 0.104,0.256 0.164,0.381 0.117,-0.127 0.211,-0.272 0.283,-0.428 -0.011,-0.03 -0.022,-0.06 -0.032,-0.089 m -0.924,-0.746 c 0.124,0.125 0.237,0.283 0.334,0.478 0.067,-0.105 0.117,-0.22 0.151,-0.339 -0.146,-0.088 -0.316,-0.137 -0.485,-0.139 m -0.118,0.008 c -0.066,0.008 -0.131,0.023 -0.194,0.046 -0.009,0.004 -0.018,0.009 -0.027,0.013 l 0.318,0.771 c 0.006,-0.004 0.013,-0.007 0.019,-0.012 0.107,-0.071 0.199,-0.163 0.277,-0.266 -0.001,-0.003 -0.003,-0.006 -0.004,-0.009 -0.085,-0.178 -0.213,-0.387 -0.389,-0.543 m -0.301,0.098 c -0.038,0.02 -0.074,0.043 -0.107,0.069 -0.052,0.234 -0.051,0.479 0.012,0.711 0.012,0.042 0.026,0.083 0.04,0.125 0.132,-0.023 0.258,-0.065 0.375,-0.128 z m -0.18,0.803 c -0.057,-0.207 -0.067,-0.424 -0.035,-0.636 -0.184,0.195 -0.286,0.471 -0.257,0.738 0.108,0.018 0.218,0.025 0.328,0.014 -0.013,-0.038 -0.026,-0.077 -0.036,-0.116 m -0.152,0.539 c 0.048,0.108 0.095,0.219 0.124,0.335 0.127,0.007 0.254,0.001 0.379,-0.02 -0.042,-0.121 -0.097,-0.239 -0.154,-0.361 -0.045,-0.095 -0.089,-0.192 -0.128,-0.292 -0.115,0.014 -0.231,0.011 -0.345,-0.005 0.026,0.117 0.076,0.232 0.124,0.343 m 0.027,1.026 c 0.187,0.032 0.365,0.046 0.532,0.04 0.022,-0.159 0.035,-0.313 0.015,-0.469 -0.009,-0.067 -0.024,-0.133 -0.043,-0.197 -0.128,0.023 -0.259,0.031 -0.389,0.025 0.004,0.027 0.007,0.053 0.008,0.08 0.008,0.184 -0.051,0.355 -0.123,0.521 m -0.058,0.13 c -0.043,0.093 -0.087,0.189 -0.12,0.285 -0.033,0.096 -0.048,0.194 -0.051,0.293 0.167,0.078 0.344,0.133 0.526,0.163 0.055,0.009 0.11,0.016 0.166,0.02 -0.007,-0.078 -0.011,-0.156 -0.009,-0.233 0.001,-0.013 0.001,-0.026 0.002,-0.038 0.006,-0.138 0.029,-0.276 0.05,-0.41 0.004,-0.027 0.008,-0.053 0.013,-0.08 -0.176,0.005 -0.362,-0.011 -0.557,-0.045 -0.007,0.015 -0.014,0.03 -0.02,0.045 m 0.08,1.458 c 0.007,0.013 0.014,0.025 0.021,0.038 0.107,0.042 0.217,0.075 0.33,0.101 0.114,0.025 0.23,0.042 0.346,0.049 C -2.673,1.198 -2.741,1.009 -2.785,0.823 -2.803,0.746 -2.817,0.67 -2.827,0.594 -2.903,0.588 -2.978,0.579 -3.053,0.565 -3.216,0.536 -3.375,0.487 -3.526,0.42 c 0.016,0.265 0.117,0.533 0.249,0.781 m 0.329,0.521 c 0.11,0.166 0.289,0.272 0.509,0.316 0.087,0.018 0.179,0.026 0.277,0.025 C -2.196,2.018 -2.229,1.972 -2.261,1.926 -2.34,1.812 -2.415,1.695 -2.484,1.574 -2.501,1.543 -2.516,1.512 -2.533,1.481 -2.662,1.476 -2.792,1.458 -2.919,1.431 -3.01,1.412 -3.1,1.387 -3.189,1.357 c 0.079,0.132 0.162,0.255 0.241,0.365 m 0.893,0.336 C -1.767,2.037 -1.445,1.94 -1.132,1.767 L -1.183,1.642 -1.357,1.222 c -0.331,0.174 -0.702,0.261 -1.074,0.261 0.009,0.016 0.016,0.031 0.025,0.047 0.077,0.137 0.164,0.27 0.255,0.398 0.032,0.044 0.063,0.087 0.096,0.13 M -1.601,0.317 C -1.61,0.322 -1.619,0.326 -1.627,0.331 l 0.315,0.764 c 0.32,-0.189 0.585,-0.461 0.774,-0.779 -0.116,-0.219 -0.253,-0.428 -0.409,-0.621 -0.171,0.251 -0.393,0.467 -0.654,0.622 m -0.597,-1.556 c -0.02,0.013 -0.041,0.024 -0.061,0.035 l 0.269,0.655 c 0.174,-0.098 0.327,-0.229 0.453,-0.384 -0.109,-0.114 -0.214,-0.234 -0.301,-0.367 -0.032,-0.048 -0.061,-0.098 -0.088,-0.148 -0.083,0.079 -0.174,0.149 -0.272,0.209 m -0.095,-0.047 c 0.016,-0.01 0.032,-0.019 0.048,-0.028 0.1,-0.062 0.193,-0.135 0.277,-0.216 -0.067,-0.137 -0.125,-0.277 -0.181,-0.416 -0.035,-0.086 -0.071,-0.174 -0.109,-0.261 -0.078,0.094 -0.168,0.178 -0.269,0.246 -0.011,0.008 -0.023,0.015 -0.035,0.022 z m -0.079,0.041 -0.268,-0.649 c -0.117,0.062 -0.244,0.106 -0.375,0.13 0.037,0.09 0.078,0.18 0.119,0.267 0.057,0.124 0.116,0.25 0.161,0.381 0.126,-0.028 0.247,-0.072 0.363,-0.129 m -0.305,0.893 c 0.194,-0.015 0.373,-0.055 0.537,-0.124 0.024,-0.009 0.048,-0.021 0.071,-0.032 l -0.269,-0.654 c -0.118,0.058 -0.242,0.102 -0.37,0.131 0.02,0.067 0.035,0.134 0.044,0.204 0.021,0.159 0.009,0.318 -0.013,0.475 m -0.041,1.06 c 0.043,0.223 0.123,0.452 0.24,0.685 0.376,0.008 0.753,-0.076 1.087,-0.254 L -1.706,0.374 c -0.289,0.147 -0.613,0.222 -0.939,0.225 -0.03,0 -0.061,0 -0.091,-0.001 0.005,0.036 0.011,0.073 0.018,0.11 M -2.754,0.237 c -0.005,0.09 -10e-4,0.18 0.007,0.272 0.058,0.002 0.115,0.002 0.173,-0.001 0.29,-0.012 0.577,-0.085 0.834,-0.217 l -0.149,-0.362 -0.137,-0.332 -0.009,-0.023 c -0.023,0.011 -0.047,0.023 -0.071,0.033 -0.178,0.074 -0.373,0.116 -0.583,0.131 -0.006,0.033 -0.011,0.066 -0.016,0.099 -0.017,0.107 -0.034,0.216 -0.044,0.324 -0.002,0.026 -0.004,0.051 -0.005,0.076 m 1.748,-0.614 c -0.037,-0.044 -0.074,-0.087 -0.113,-0.128 -0.057,-0.062 -0.117,-0.123 -0.177,-0.183 -0.017,-0.017 -0.035,-0.035 -0.053,-0.053 -0.042,-0.042 -0.085,-0.084 -0.127,-0.127 -0.133,0.162 -0.296,0.3 -0.479,0.401 l 0.008,0.021 0.137,0.331 0.149,0.363 c 0.005,-0.003 0.01,-0.005 0.014,-0.008 0.257,-0.151 0.473,-0.365 0.639,-0.613 0.001,-0.001 0.001,-0.003 0.002,-0.004 M -1.054,1.722 C -1.014,1.699 -0.975,1.674 -0.935,1.648 -0.652,1.461 -0.42,1.234 -0.254,0.997 -0.271,0.943 -0.289,0.889 -0.308,0.835 -0.36,0.69 -0.42,0.548 -0.489,0.409 -0.686,0.723 -0.956,0.991 -1.278,1.178 l 0.173,0.42 z m 0.168,0.001 c -0.832,0.55 -1.79,0.572 -2.136,0.05 -0.085,-0.12 -0.177,-0.255 -0.261,-0.4 -0.245,-0.418 -0.435,-0.917 -0.278,-1.374 0.034,-0.1 0.079,-0.198 0.122,-0.293 0.095,-0.205 0.183,-0.399 0.174,-0.61 -0.007,-0.163 -0.073,-0.314 -0.142,-0.473 -0.051,-0.117 -0.104,-0.239 -0.133,-0.367 -0.089,-0.41 0.119,-0.859 0.475,-1.073 0.006,-0.022 0.01,-0.019 0.016,-0.018 0.005,0 0.02,-0.003 0.02,-0.003 0.036,-0.019 0.073,-0.037 0.111,-0.052 0.172,-0.065 0.359,-0.075 0.539,-0.038 0.282,0.057 0.544,0.23 0.686,0.484 0.053,0.095 0.09,0.199 0.126,0.298 0.04,0.113 0.078,0.219 0.137,0.315 0.133,0.217 0.361,0.359 0.581,0.497 0.046,0.029 0.092,0.058 0.137,0.087 0.163,0.107 0.396,0.406 0.569,0.701 0.115,0.196 0.205,0.39 0.23,0.525 l 0.001,0.006 C 0.24,0.519 -0.168,1.248 -0.886,1.723" /></g><g
+           transform="translate(10.1347,13.3765)"
+           id="g192"><path
+             id="path194"
+             style="fill:#222260;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 c 0,0 1.266,-0.746 3.119,-0.436 1.853,0.311 3.459,2.569 3.459,2.569 L 8.382,1.194 c 0,0 -2.988,-1.757 -4.465,-2.214 -1.478,-0.457 -3.249,0.579 -4.192,0.979 z" /></g><g
+           transform="translate(16.1919,14.6104)"
+           id="g196"><path
+             id="path198"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 1.069,-0.512 -0.243,-0.195 -1.06,0.502 z" /></g><g
+           transform="translate(10.3721,13.3692)"
+           id="g200"><path
+             id="path202"
+             style="fill:#222260;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -3.67,-3.195 c 0,0 0.054,0.689 0.302,1.137 0,0 0.959,1.094 1.657,1.488 0.699,0.394 1.276,0.577 1.394,0.572 C -0.271,0.001 -0.243,0.01 -0.188,0.01 -0.105,0.01 0,0 0,0" /></g><g
+           transform="translate(15.1905,15.9486)"
+           id="g204"><path
+             id="path206"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="M 0,0 C 0,0 0.206,0.272 0.291,0.419 0.379,0.57 0.516,0.894 0.516,0.894" /></g><g
+           transform="translate(15.3113,15.8571)"
+           id="g208"><path
+             id="path210"
+             style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             d="m 0,0 -0.242,0.183 c 0.002,0.003 0.201,0.266 0.281,0.403 0.082,0.142 0.215,0.455 0.216,0.458 L 0.534,0.926 C 0.529,0.913 0.393,0.592 0.301,0.434 0.212,0.281 0.009,0.011 0,0" /></g></g></g></g></svg>
\ No newline at end of file
diff --git a/java/Main.java b/java/Main.java
index c04db2c..e824a95 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -16,7 +16,7 @@
 public final class Main {
   private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
   private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
-  private static final Runtime.Version MIN_JAVA_VERSION = Runtime.Version.parse("11.0.10");
+  private static final Runtime.Version MIN_JAVA_VERSION = Runtime.Version.parse("17.0.5");
 
   // We don't do any real work here because we need to import
   // the archive lookup code and we cannot import a class in
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index b4b9935..96a6d32 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -19,8 +19,6 @@
 import static com.google.common.truth.OptionalSubject.optionals;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
@@ -35,7 +33,6 @@
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
@@ -49,15 +46,13 @@
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Chars;
 import com.google.common.testing.FakeTicker;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
-import com.google.gerrit.acceptance.config.GerritSystemProperty;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -146,12 +141,11 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.change.Revisions;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.util.git.DelegateSystemReader;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.SshMode;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -160,8 +154,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Modifier;
-import java.sql.Timestamp;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -190,10 +182,7 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.SystemReader;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -207,20 +196,11 @@
 @RunWith(ConfigSuite.class)
 public abstract class AbstractDaemonTest {
 
-  /**
-   * Test methods without special annotations will use a common server for efficiency reasons. The
-   * server is torn down after the test class is done.
-   */
-  private static GerritServer commonServer;
-
-  private static Description firstTest;
-
   @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   @ConfigSuite.Parameter public Config baseConfig;
-  @ConfigSuite.Name private String configName;
+  @ConfigSuite.Name public String configName;
 
-  @Rule
   public TestRule testRunner =
       new TestRule() {
         @Override
@@ -228,18 +208,12 @@
           return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-              if (firstTest == null) {
-                firstTest = description;
-              }
               beforeTest(description);
-              ProjectResetter.Config input = requireNonNull(resetProjects());
-
               try (ProjectResetter resetter =
-                  projectResetter != null ? projectResetter.builder().build(input) : null) {
-                AbstractDaemonTest.this.resetter = resetter;
+                  server.createProjectResetter(
+                      (allProjects, allUsers) -> resetProjects(allProjects, allUsers))) {
                 base.evaluate();
               } finally {
-                AbstractDaemonTest.this.resetter = null;
                 afterTest();
               }
             }
@@ -247,11 +221,36 @@
         }
       };
 
+  protected DaemonTestRule daemonTestRule;
+  protected TestConfigRule configRule;
+  protected ServerTestRule server;
+  protected TimeSettingsTestRule timeSettingsRule;
+
+  @Rule public TestRule topLevelTestRule = createTopLevelTestRule();
+
+  /**
+   * Creates test rules required for tests.
+   *
+   * <p>Creating all rules in a single method gives more flexibility in the order of creation and
+   * allows additional initialization steps if needed.
+   */
+  protected TestRule createTopLevelTestRule() {
+    daemonTestRule = createDaemonTestRule();
+    configRule = daemonTestRule.configRule();
+    timeSettingsRule = daemonTestRule.timeSettingsRule();
+    server = daemonTestRule.server();
+    return daemonTestRule;
+  }
+
+  @UsedAt(UsedAt.Project.GOOGLE)
+  protected DaemonTestRule createDaemonTestRule() {
+    return GerritServerDaemonTestRule.create(this);
+  }
+
   @Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl;
   @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
   @Inject @GerritServerConfig protected Config cfg;
   @Inject @GerritInstanceId @Nullable protected String instanceId;
-  @Inject protected AcceptanceTestRequestScope atrScope;
   @Inject protected AccountCache accountCache;
   @Inject protected AccountCreator accountCreator;
   @Inject protected Accounts accounts;
@@ -273,7 +272,6 @@
   @Inject protected PatchSetUtil psUtil;
   @Inject protected ProjectCache projectCache;
   @Inject protected ProjectConfig.Factory projectConfigFactory;
-  @Inject protected ProjectResetter.Builder.Factory projectResetter;
   @Inject protected Provider<InternalChangeQuery> queryProvider;
   @Inject protected PushOneCommit.Factory pushFactory;
   @Inject protected PluginConfigFactory pluginConfig;
@@ -283,25 +281,22 @@
   @Inject protected BatchAbandon batchAbandon;
   @Inject protected TestSshKeys sshKeys;
   @Inject protected TestTicker testTicker;
+  @Inject protected ThreadLocalRequestContext localCtx;
+
+  @Nullable public SshSession adminSshSession;
+
+  @Nullable public SshSession userSshSession;
 
   protected EventRecorder eventRecorder;
 
-  protected GerritServer server;
-
   protected Project.NameKey project;
   protected RestSession adminRestSession;
   protected RestSession userRestSession;
   protected RestSession anonymousRestSession;
-  protected SshSession adminSshSession;
-  protected SshSession userSshSession;
   protected TestAccount admin;
   protected TestAccount user;
   protected TestRepository<InMemoryRepository> testRepo;
   protected String resourcePrefix;
-  protected Description description;
-  protected GerritServer.Description testMethodDescription;
-
-  protected boolean testRequiresSsh;
   protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
 
   @Inject private AbstractChangeNotes.Args changeNotesArgs;
@@ -314,54 +309,8 @@
   @Inject private SitePaths sitePaths;
   @Inject private ProjectOperations projectOperations;
 
-  private ProjectResetter resetter;
   private List<Repository> toClose;
-  private String systemTimeZone;
-  private SystemReader oldSystemReader;
 
-  /**
-   * The Getters and Setters below are needed for tests that run on custom {@link GerritServer}
-   * (that can be set up via {@link #initServer} and {@link #setUpDatabase} methods. Because tests
-   * inherit directly from {@link AbstractDaemonTest}, the set up has to be delegated to some other
-   * class that can share the set up logic across different test classes.
-   *
-   * <p>E.g, we need to be able to do something like:
-   *
-   * <pre>{@code
-   * public class AccountIT extends AbstractDaemonTest {...}
-   *
-   * public class AbstractDaemonTestAdapter {
-   *
-   *   protected void initServer() {...}
-   *
-   *   ...
-   *
-   * }
-   *
-   * public class CustomAccountIT extends AccountIT {
-   *
-   *   AbstractDaemonTestAdapter testAdapter;
-   *
-   *   {@literal @Override}
-   *   protected void initServer() {
-   *         testAdapter.initServer();
-   *   }
-   *   ...
-   * }
-   *
-   * public class CustomChangeIT extends ChangeIT {
-   *
-   *   AbstractDaemonTestAdapter testAdapter;
-   *
-   *   {@literal @Override}
-   *   protected void initServer() {
-   *         testAdapter.initServer();
-   *   }
-   *   ...
-   * }
-   *
-   * }</pre>
-   */
   public String getResourcePrefix() {
     return resourcePrefix;
   }
@@ -370,10 +319,6 @@
     this.resourcePrefix = resourcePrefix;
   }
 
-  public Description getDescription() {
-    return description;
-  }
-
   public TestRepository<InMemoryRepository> getTestRepo() {
     return testRepo;
   }
@@ -406,14 +351,6 @@
     this.project = project;
   }
 
-  public GerritServer getServer() {
-    return server;
-  }
-
-  public void setServer(GerritServer server) {
-    this.server = server;
-  }
-
   @Before
   public void clearSender() {
     if (sender != null) {
@@ -428,19 +365,9 @@
     }
   }
 
-  @Before
-  public void assumeSshIfRequired() {
-    if (testRequiresSsh) {
-      // If the test uses ssh, we use assume() to make sure ssh is enabled on
-      // the test suite. JUnit will skip tests annotated with @UseSsh if we
-      // disable them using the command line flag.
-      assume().that(SshMode.useSsh()).isTrue();
-    }
-  }
-
   @After
   public void verifyNoPiiInChangeNotes() throws RestApiException, IOException {
-    if (testMethodDescription.verifyNoPiiInChangeNotes()) {
+    if (configRule.methodDescription().verifyNoPiiInChangeNotes()) {
       verifyNoAccountDetailsInChangeNotes();
     }
   }
@@ -454,23 +381,18 @@
 
   @ConfigSuite.AfterConfig
   public static void stopCommonServer() throws Exception {
-    if (commonServer != null) {
-      try {
-        commonServer.close();
-      } catch (Exception e) {
-        throw new AssertionError(
-            "Error stopping common server in "
-                + (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
-            e);
-      } finally {
-        commonServer = null;
-      }
-    }
+    GerritServerTestRule.afterConfigChanged();
   }
 
-  /** Controls which project and branches should be reset after each test case. */
-  protected ProjectResetter.Config resetProjects() {
-    return new ProjectResetter.Config()
+  /**
+   * Controls which project and branches should be reset in the commonServer after each test case.
+   *
+   * <p>Parameters allProjects and allUsers must refer to the commonServer names - if a test doesn't
+   * use commonServer then names in the test can be different from names in commonServer.
+   */
+  protected ProjectResetter.Config resetProjects(
+      AllProjectsName allProjects, AllUsersName allUsers) {
+    return new ProjectResetter.Config.Builder()
         // Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
         // not reused.
         .reset(allProjects, RefNames.REFS_CONFIG)
@@ -483,26 +405,20 @@
             RefNames.REFS_GROUPNAMES,
             RefNames.REFS_GROUPS + "*",
             RefNames.REFS_STARRED_CHANGES + "*",
-            RefNames.REFS_DRAFT_COMMENTS + "*");
+            RefNames.REFS_DRAFT_COMMENTS + "*")
+        .build();
   }
 
   protected void restartAsSlave() throws Exception {
-    closeSsh();
-    server = GerritServer.restartAsSlave(server);
+    server.restartAsSlave();
     server.getTestInjector().injectMembers(this);
-    if (resetter != null) {
-      server.getTestInjector().injectMembers(resetter);
-    }
-    initSsh();
+    updateSshSessions();
   }
 
   protected void restart() throws Exception {
-    server = GerritServer.restart(server, createModule(), createSshModule());
+    server.restart();
     server.getTestInjector().injectMembers(this);
-    if (resetter != null) {
-      server.getTestInjector().injectMembers(resetter);
-    }
-    initSsh();
+    updateSshSessions();
   }
 
   public void reindexAccount(Account.Id accountId) {
@@ -526,49 +442,58 @@
   protected void beforeTest(Description description) throws Exception {
     // SystemReader must be overridden before creating any repos, since they read the user/system
     // configs at initialization time, and are then stored in the RepositoryCache forever.
-    oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
 
-    this.description = description;
-    GerritServer.Description classDesc =
-        GerritServer.Description.forTestClass(description, configName);
-    GerritServer.Description methodDesc =
-        GerritServer.Description.forTestMethod(description, configName);
-    testMethodDescription = methodDesc;
-
-    if (methodDesc.systemProperties() != null) {
-      ConfigAnnotationParser.parse(methodDesc.systemProperties());
+    if (enableExperimentsRejectImplicitMergesOnMerge()) {
+      // When changes are merged/submitted - reject the operation if there is an implicit merge (
+      // even if rejectImplicitMerges is disabled in the project config).
+      baseConfig.setStringList(
+          "experiments",
+          null,
+          "enabled",
+          ImmutableList.of(
+              "GerritBackendFeature__check_implicit_merges_on_merge",
+              "GerritBackendFeature__reject_implicit_merges_on_merge",
+              "GerritBackendFeature__always_reject_implicit_merges_on_merge"));
     }
 
-    if (methodDesc.systemProperty() != null) {
-      ConfigAnnotationParser.parse(methodDesc.systemProperty());
-    }
-
-    testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
-    if (!testRequiresSsh) {
-      baseConfig.setString("sshd", null, "listenAddress", "off");
-    }
-
-    baseConfig.unset("gerrit", null, "canonicalWebUrl");
-    baseConfig.unset("httpd", null, "listenUrl");
-
-    baseConfig.setInt("index", null, "batchThreads", -1);
-
-    initServer(classDesc, methodDesc);
-
+    server.initServer();
     server.getTestInjector().injectMembers(this);
+
     Transport.register(inProcessProtocol);
     toClose = Collections.synchronizedList(new ArrayList<>());
 
-    setUpDatabase(classDesc);
-
-    // Set the clock step last, so that the test setup isn't consuming any timestamps after the
-    // clock has been set.
-    setTimeSettings(classDesc.useSystemTime(), classDesc.useClockStep(), classDesc.useTimezone());
-    setTimeSettings(
-        methodDesc.useSystemTime(), methodDesc.useClockStep(), methodDesc.useTimezone());
+    setUpDatabase();
+    server.initSsh();
+    updateSshSessions();
   }
 
-  protected void setUpDatabase(GerritServer.Description classDesc) throws Exception {
+  void updateSshSessions() throws Exception {
+    userSshSession = null;
+    adminSshSession = null;
+    if (server.sshInitialized()) {
+      try (ManualRequestContext ctx = requestScopeOperations.setNestedApiUser(user.id())) {
+        userSshSession = server.getOrCreateSshSessionForContext(ctx);
+        // The session doesn't store any reference to the context and it remains open after the ctx
+        // is closed.
+        userSshSession.open();
+      }
+
+      try (ManualRequestContext ctx = requestScopeOperations.setNestedApiUser(admin.id())) {
+        adminSshSession = server.getOrCreateSshSessionForContext(ctx);
+        // The session doesn't store any reference to the context and it remains open after the ctx
+        // is closed.
+        adminSshSession.open();
+      }
+    }
+  }
+
+  protected boolean enableExperimentsRejectImplicitMergesOnMerge() {
+    // By default any attempt to make an explicit merge is rejected. This allows to check
+    // that existing workflows continue to work even if gerrit rejects implicit merges on merge.
+    return true;
+  }
+
+  protected void setUpDatabase() throws Exception {
     admin = accountCreator.admin();
     user = accountCreator.user1();
 
@@ -576,90 +501,22 @@
     reindexAccount(admin.id());
     reindexAccount(user.id());
 
-    adminRestSession = new RestSession(server, admin);
-    userRestSession = new RestSession(server, user);
-    anonymousRestSession = new RestSession(server, null);
+    adminRestSession = server.createRestSession(admin);
+    userRestSession = server.createRestSession(user);
+    anonymousRestSession = server.createRestSession(null);
 
-    initSsh();
-
-    String testMethodName = description.getMethodName();
+    String testMethodName = configRule.description().getMethodName();
     resourcePrefix =
         UNSAFE_PROJECT_NAME
-            .matcher(description.getClassName() + "_" + testMethodName + "_")
+            .matcher(configRule.description().getClassName() + "_" + testMethodName + "_")
             .replaceAll("");
 
-    setRequestScope(admin);
-    ProjectInput in = projectInput(description);
+    requestScopeOperations.setApiUser(admin.id());
+    ProjectInput in = projectInput(configRule.description());
     gApi.projects().create(in);
     project = Project.nameKey(in.name);
-    if (!classDesc.skipProjectClone()) {
-      testRepo = cloneProject(project, getCloneAsAccount(description));
-    }
-  }
-
-  protected void initServer(GerritServer.Description classDesc, GerritServer.Description methodDesc)
-      throws Exception {
-    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, auditModule, sshModule);
-      }
-      server = commonServer;
-    } else {
-      server =
-          GerritServer.initAndStart(
-              temporaryFolder, methodDesc, baseConfig, module, auditModule, sshModule);
-    }
-  }
-
-  private static SystemReader setFakeSystemReader(File tempDir) {
-    SystemReader oldSystemReader = SystemReader.getInstance();
-    SystemReader.setInstance(
-        new DelegateSystemReader(oldSystemReader) {
-          @Override
-          public FileBasedConfig openJGitConfig(Config parent, FS fs) {
-            return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
-          }
-
-          @Override
-          public FileBasedConfig openUserConfig(Config parent, FS fs) {
-            return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
-          }
-
-          @Override
-          public FileBasedConfig openSystemConfig(Config parent, FS fs) {
-            return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
-          }
-        });
-    return oldSystemReader;
-  }
-
-  private void setTimeSettings(
-      boolean useSystemTime,
-      @Nullable UseClockStep useClockStep,
-      @Nullable UseTimezone useTimezone) {
-    if (useSystemTime) {
-      TestTimeUtil.useSystemTime();
-    } else if (useClockStep != null) {
-      TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
-      if (useClockStep.startAtEpoch()) {
-        TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
-      }
-    }
-    if (useTimezone != null) {
-      systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
-    }
-  }
-
-  private void resetTimeSettings() {
-    TestTimeUtil.useSystemTime();
-    if (systemTimeZone != null) {
-      System.setProperty("user.timezone", systemTimeZone);
-      systemTimeZone = null;
+    if (!configRule.classDescription().skipProjectClone()) {
+      testRepo = cloneProject(project, getCloneAsAccount(configRule.description()));
     }
   }
 
@@ -678,23 +535,6 @@
     return null;
   }
 
-  protected void initSsh() throws Exception {
-    if (testRequiresSsh
-        && SshMode.useSsh()
-        && (adminSshSession == null || userSshSession == null)) {
-      // Create Ssh sessions
-      SshSessionFactory.initSsh();
-      Context ctx = newRequestContext(user);
-      atrScope.set(ctx);
-      userSshSession = ctx.getSession();
-      userSshSession.open();
-      ctx = newRequestContext(admin);
-      atrScope.set(ctx);
-      adminSshSession = ctx.getSession();
-      adminSshSession.open();
-    }
-  }
-
   protected TestAccount getCloneAsAccount(Description description) {
     TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
     return accountCreator.get(ann != null ? ann.cloneAs() : "admin");
@@ -754,9 +594,10 @@
    * @return name prefixed by a string unique to this test method.
    */
   protected String name(String name) {
-    return resourcePrefix + name;
+    return daemonTestRule.name(name);
   }
 
+  @CanIgnoreReturnValue
   protected Project.NameKey createProjectOverAPI(
       String nameSuffix,
       @Nullable Project.NameKey parent,
@@ -778,7 +619,7 @@
 
   protected TestRepository<InMemoryRepository> cloneProject(
       Project.NameKey p, TestAccount testAccount) throws Exception {
-    return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount));
+    return daemonTestRule.cloneProject(p, testAccount);
   }
 
   /**
@@ -786,8 +627,7 @@
    *
    * @return a URI string that can be used to connect to this repository for both fetch and push.
    */
-  protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
-      throws Exception {
+  String registerRepoConnection(Project.NameKey p, TestAccount testAccount) throws Exception {
     InProcessProtocol.Context ctx =
         new InProcessProtocol.Context(identifiedUserFactory, testAccount.id(), p);
     Repository repo = repoManager.openRepository(p);
@@ -800,42 +640,11 @@
     for (Repository repo : toClose) {
       repo.close();
     }
-    closeSsh();
-    resetTimeSettings();
-    if (server != commonServer) {
-      server.close();
-      server = null;
-    }
 
-    GerritServer.Description methodDesc =
-        GerritServer.Description.forTestMethod(description, configName);
-    if (methodDesc.systemProperties() != null) {
-      for (GerritSystemProperty sysProp : methodDesc.systemProperties().value()) {
-        System.clearProperty(sysProp.name());
-      }
-    }
-
-    if (methodDesc.systemProperty() != null) {
-      System.clearProperty(methodDesc.systemProperty().name());
-    }
-
-    SystemReader.setInstance(oldSystemReader);
-    oldSystemReader = null;
     // Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
     testTicker.useDefaultTicker();
   }
 
-  protected void closeSsh() {
-    if (adminSshSession != null) {
-      adminSshSession.close();
-      adminSshSession = null;
-    }
-    if (userSshSession != null) {
-      userSshSession.close();
-      userSshSession = null;
-    }
-  }
-
   /**
    * Verify that NoteDB commits do not persist user-sensitive information, by running checks for all
    * commits in {@link RefNames#changeMetaRef} for all changes, created during the test.
@@ -874,7 +683,7 @@
             if (accountState.userName().isPresent()) {
               assertThat(fullMessage).doesNotContain(accountState.userName().get());
             }
-            List<String> allEmails =
+            ImmutableList<String> allEmails =
                 accountState.externalIds().stream()
                     .map(ExternalId::email)
                     .filter(Objects::nonNull)
@@ -906,10 +715,12 @@
     return b;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChange() throws Exception {
     return createChange("refs/for/master");
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChange(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result result = push.to(ref);
@@ -917,6 +728,7 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChange(TestRepository<InMemoryRepository> repo)
       throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -925,10 +737,12 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createMergeCommitChange(String ref) throws Exception {
     return createMergeCommitChange(ref, "foo");
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createMergeCommitChange(String ref, String file) throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
 
@@ -962,6 +776,7 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createNParentsMergeCommitChange(String ref, List<String> fileNames)
       throws Exception {
     // This method creates n different commits and creates a merge commit pointing to all n parents.
@@ -1013,6 +828,7 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createCommitAndPush(
       TestRepository<InMemoryRepository> repo,
       String ref,
@@ -1026,6 +842,7 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChangeWithTopic(
       TestRepository<InMemoryRepository> repo,
       String topic,
@@ -1038,12 +855,14 @@
         repo, "refs/for/master%topic=" + name(topic), commitMsg, fileName, content);
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChange(String subject, String fileName, String content)
       throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/master");
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result createChange(
       TestRepository<?> repo,
       String branch,
@@ -1057,6 +876,7 @@
         "refs/for/" + branch + (Strings.isNullOrEmpty(topic) ? "" : "%topic=" + name(topic)));
   }
 
+  @CanIgnoreReturnValue
   protected BranchApi createBranch(BranchNameKey branch) throws Exception {
     return gApi.projects()
         .name(branch.project().get())
@@ -1064,6 +884,7 @@
         .create(new BranchInput());
   }
 
+  @CanIgnoreReturnValue
   protected BranchApi createBranchWithRevision(BranchNameKey branch, String revision)
       throws Exception {
     BranchInput in = new BranchInput();
@@ -1074,6 +895,7 @@
   private static final List<Character> RANDOM =
       Chars.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result amendChangeWithUploader(
       PushOneCommit.Result change, Project.NameKey projectName, TestAccount account)
       throws Exception {
@@ -1092,10 +914,12 @@
     return result;
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result amendChange(String changeId) throws Exception {
     return amendChange(changeId, "refs/for/master", admin, testRepo);
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result amendChange(
       String changeId, String ref, TestAccount testAccount, TestRepository<?> repo)
       throws Exception {
@@ -1110,11 +934,13 @@
         new String(Chars.toArray(RANDOM)));
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result amendChange(
       String changeId, String subject, String fileName, String content) throws Exception {
     return amendChange(changeId, "refs/for/master", admin, testRepo, subject, fileName, content);
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result amendChange(
       String changeId,
       String ref,
@@ -1158,17 +984,6 @@
     return gApi.changes().query(q).get();
   }
 
-  /** Sets up {@code account} as a caller in tests. */
-  public void setRequestScope(TestAccount account) {
-    Context ctx = newRequestContext(account);
-    atrScope.set(ctx);
-  }
-
-  protected Context newRequestContext(TestAccount account) {
-    requestScopeOperations.setApiUser(account.id());
-    return atrScope.get();
-  }
-
   protected Account getAccount(Account.Id accountId) {
     return getAccountState(accountId).account();
   }
@@ -1184,10 +999,8 @@
 
   protected AutoCloseable disableNoteDb() {
     changeNotesArgs.failOnLoadForTest.set(true);
-    Context oldContext = atrScope.disableNoteDb();
     return () -> {
       changeNotesArgs.failOnLoadForTest.set(false);
-      atrScope.set(oldContext);
     };
   }
 
@@ -1240,6 +1053,7 @@
         .update();
   }
 
+  @CanIgnoreReturnValue
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
@@ -1358,7 +1172,7 @@
   protected ChangeResource parseChangeResource(String changeId) throws Exception {
     List<ChangeNotes> notes = changeFinder.find(changeId);
     assertThat(notes).hasSize(1);
-    return changeResourceFactory.create(notes.get(0), atrScope.get().getUser());
+    return changeResourceFactory.create(notes.get(0), localCtx.getContext().getUser());
   }
 
   protected RevCommit getHead(Repository repo, String name) throws Exception {
@@ -1736,6 +1550,7 @@
     assertThat(res).isEqualTo(expectedContent);
   }
 
+  @CanIgnoreReturnValue
   protected RevCommit createNewCommitWithoutChangeId(String branch, String file, String content)
       throws Exception {
     try (Repository repo = repoManager.openRepository(project);
@@ -1831,7 +1646,7 @@
     return new ProjectConfigUpdate(projectName);
   }
 
-  protected class ProjectConfigUpdate implements AutoCloseable {
+  public class ProjectConfigUpdate implements AutoCloseable {
     private final ProjectConfig projectConfig;
     private MetaDataUpdate metaDataUpdate;
 
@@ -1953,12 +1768,14 @@
     }
 
     /** Switches to system ticker */
+    @CanIgnoreReturnValue
     public Ticker useDefaultTicker() {
       this.actualTicker = Ticker.systemTicker();
       return actualTicker;
     }
 
     /** Switches to {@link FakeTicker} */
+    @CanIgnoreReturnValue
     public FakeTicker useFakeTicker() {
       if (!(this.actualTicker instanceof FakeTicker)) {
         this.actualTicker = new FakeTicker();
diff --git a/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java b/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
index 4e8d20d..11f2a41 100644
--- a/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
@@ -82,6 +82,10 @@
   }
 
   public static class PluginOneSshModule extends CommandModule {
+    public PluginOneSshModule() {
+      super(/* slaveMode= */ false);
+    }
+
     @Override
     public void configure() {
       command(LS_SAMPLES).to(ListSamplesCommand.class);
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 8a9e56a..7681734 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -26,6 +26,7 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Address;
@@ -41,6 +42,8 @@
 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.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
@@ -67,10 +70,11 @@
   }
 
   @Override
-  protected ProjectResetter.Config resetProjects() {
+  protected ProjectResetter.Config resetProjects(
+      AllProjectsName allProjects, AllUsersName allUsers) {
     // Don't reset anything so that stagedUsers can be cached across all tests.
     // Without this caching these tests become much too slow.
-    return new ProjectResetter.Config();
+    return new ProjectResetter.Config.Builder().build();
   }
 
   protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
@@ -109,14 +113,14 @@
       fakeEmailSender = target;
     }
 
-    public FakeEmailSenderSubject didNotSend() {
+    public void didNotSend() {
       Message message = fakeEmailSender.peekMessage();
       if (message != null) {
         failWithoutActual(fact("expected no message", message));
       }
-      return this;
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
       message = fakeEmailSender.nextMessage();
       if (message == null) {
@@ -183,18 +187,22 @@
       return addrList.getAddressList().stream().map(Address::email).collect(toList());
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject to(String... emails) {
       return rcpt(users.supportReviewersByEmail ? TO : null, emails);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject cc(String... emails) {
       return rcpt(users.supportReviewersByEmail ? CC : null, emails);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject bcc(String... emails) {
       return rcpt(users.supportReviewersByEmail ? BCC : null, emails);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject title(String expectedEmailTitle) {
       if (!emailTitle.equals(expectedEmailTitle)) {
         failWithoutActual(
@@ -204,6 +212,7 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, String[] emails) {
       for (String email : emails) {
         rcpt(type, email);
@@ -230,6 +239,7 @@
       }
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject noOneElse() {
       for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
         if (!accountedFor.contains(watchEntry.getValue().email())) {
@@ -257,22 +267,27 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject notTo(String... emails) {
       return rcpt(null, emails);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject to(TestAccount... accounts) {
       return rcpt(TO, accounts);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject cc(TestAccount... accounts) {
       return rcpt(CC, accounts);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject bcc(TestAccount... accounts) {
       return rcpt(BCC, accounts);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject notTo(TestAccount... accounts) {
       return rcpt(null, accounts);
     }
@@ -288,18 +303,22 @@
       rcpt(type, account.email());
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject to(NotifyType... watches) {
       return rcpt(TO, watches);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject cc(NotifyType... watches) {
       return rcpt(CC, watches);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject bcc(NotifyType... watches) {
       return rcpt(BCC, watches);
     }
 
+    @CanIgnoreReturnValue
     public FakeEmailSenderSubject notTo(NotifyType... watches) {
       return rcpt(null, watches);
     }
@@ -350,7 +369,7 @@
     public boolean supportReviewersByEmail;
 
     private String usersCacheKey() {
-      return description.getClassName();
+      return configRule.description().getClassName();
     }
 
     private TestAccount reindexAndCopy(TestAccount account) {
@@ -491,10 +510,12 @@
     }
   }
 
+  @CanIgnoreReturnValue
   protected StagedPreChange stagePreChange(String ref) throws Exception {
     return new StagedPreChange(ref);
   }
 
+  @CanIgnoreReturnValue
   protected StagedPreChange stagePreChange(
       String ref, @Nullable PushOptionGenerator pushOptionGenerator) throws Exception {
     return new StagedPreChange(ref, pushOptionGenerator);
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index fe845c0..3dced64 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -20,6 +20,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.common.Nullable;
@@ -297,14 +298,15 @@
     assertThat(getter.call(id).get(id)).isNull();
   }
 
-  protected static List<PluginDefinedInfo> pluginInfoFromSingletonList(
+  protected static ImmutableList<PluginDefinedInfo> pluginInfoFromSingletonList(
       List<ChangeInfo> changeInfos) {
     assertThat(changeInfos).hasSize(1);
     return pluginInfoFromChangeInfo(changeInfos.get(0));
   }
 
   @Nullable
-  protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
+  protected static ImmutableList<PluginDefinedInfo> pluginInfoFromChangeInfo(
+      ChangeInfo changeInfo) {
     List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
     if (pluginInfo == null) {
       return null;
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
deleted file mode 100644
index c4bf20c..0000000
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance;
-
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.inject.Inject;
-import com.google.inject.Key;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.Scope;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Guice scopes for state during an Acceptance Test connection. */
-public class AcceptanceTestRequestScope {
-  private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class);
-
-  public static class Context implements RequestContext {
-    private final RequestCleanup cleanup = new RequestCleanup();
-    private final Map<Key<?>, Object> map = new HashMap<>();
-    private final SshSession session;
-    private final CurrentUser user;
-
-    final long created;
-    volatile long started;
-    volatile long finished;
-
-    private Context(SshSession s, CurrentUser u, long at) {
-      session = s;
-      user = u;
-      created = started = finished = at;
-      map.put(RC_KEY, cleanup);
-    }
-
-    private Context(Context p, SshSession s, CurrentUser c) {
-      this(s, c, p.created);
-      started = p.started;
-      finished = p.finished;
-    }
-
-    public SshSession getSession() {
-      return session;
-    }
-
-    @Override
-    public CurrentUser getUser() {
-      if (user == null) {
-        throw new IllegalStateException("user == null, forgot to set it?");
-      }
-      return user;
-    }
-
-    synchronized <T> T get(Key<T> key, Provider<T> creator) {
-      @SuppressWarnings("unchecked")
-      T t = (T) map.get(key);
-      if (t == null) {
-        t = creator.get();
-        map.put(key, t);
-      }
-      return t;
-    }
-  }
-
-  static class ContextProvider implements Provider<Context> {
-    @Override
-    public Context get() {
-      return requireContext();
-    }
-  }
-
-  static class SshSessionProvider implements Provider<SshSession> {
-    @Override
-    public SshSession get() {
-      return requireContext().getSession();
-    }
-  }
-
-  static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
-    private final AcceptanceTestRequestScope atrScope;
-
-    @Inject
-    Propagator(AcceptanceTestRequestScope atrScope, ThreadLocalRequestContext local) {
-      super(REQUEST, current, local);
-      this.atrScope = atrScope;
-    }
-
-    @Override
-    protected Context continuingContext(Context ctx) {
-      // The cleanup is not chained, since the RequestScopePropagator executors
-      // the Context's cleanup when finished executing.
-      return atrScope.newContinuingContext(ctx);
-    }
-  }
-
-  private static final ThreadLocal<Context> current = new ThreadLocal<>();
-
-  private static Context requireContext() {
-    final Context ctx = current.get();
-    if (ctx == null) {
-      throw new OutOfScopeException("Not in command/request");
-    }
-    return ctx;
-  }
-
-  private final ThreadLocalRequestContext local;
-
-  @Inject
-  AcceptanceTestRequestScope(ThreadLocalRequestContext local) {
-    this.local = local;
-  }
-
-  public Context newContext(SshSession s, CurrentUser user) {
-    return new Context(s, user, TimeUtil.nowMs());
-  }
-
-  private Context newContinuingContext(Context ctx) {
-    return new Context(ctx, ctx.getSession(), ctx.getUser());
-  }
-
-  public Context set(Context ctx) {
-    Context old = current.get();
-    current.set(ctx);
-    local.setContext(ctx);
-    return old;
-  }
-
-  public Context get() {
-    return current.get();
-  }
-
-  /**
-   * Disables read and write access to NoteDb and returns the context prior to that modification.
-   */
-  public Context disableNoteDb() {
-    Context old = current.get();
-    Context ctx = new Context(old.session, old.user, old.created);
-
-    current.set(ctx);
-    local.setContext(ctx);
-    return old;
-  }
-
-  /** Returns exactly one instance per command executed. */
-  static final Scope REQUEST =
-      new Scope() {
-        @Override
-        public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
-          return new Provider<>() {
-            @Override
-            public T get() {
-              return requireContext().get(key, creator);
-            }
-
-            @Override
-            public String toString() {
-              return String.format("%s[%s]", creator, REQUEST);
-            }
-          };
-        }
-
-        @Override
-        public String toString() {
-          return "Acceptance Test Scope.REQUEST";
-        }
-      };
-}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index f3881f2..8654f82 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.gerrit.common.UsedAt.Project.GOOGLE;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
@@ -56,7 +58,8 @@
   private final ExternalIdFactory externalIdFactory;
 
   @Inject
-  AccountCreator(
+  @UsedAt(GOOGLE)
+  protected AccountCreator(
       Sequences sequences,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       GroupCache groupCache,
@@ -87,7 +90,7 @@
     List<ExternalId> extIds = new ArrayList<>(2);
     String httpPass = null;
     if (username != null) {
-      httpPass = "http-pass";
+      httpPass = externalIdFactory.arePasswordsAllowed() ? "http-pass" : null;
       extIds.add(externalIdFactory.createUsername(username, id, httpPass));
     }
 
@@ -107,14 +110,9 @@
                     .addExternalIds(extIds));
 
     ImmutableList.Builder<String> tags = ImmutableList.builder();
+    addUserToGroups(id, groupNames);
     if (groupNames != null) {
       for (String n : groupNames) {
-        AccountGroup.NameKey k = AccountGroup.nameKey(n);
-        Optional<InternalGroup> group = groupCache.get(k);
-        if (!group.isPresent()) {
-          throw new NoSuchGroupException(n);
-        }
-        addGroupMember(group.get().getGroupUUID(), id);
         if (ServiceUserClassifier.SERVICE_USERS.equals(n)) {
           tags.add("SERVICE_USER");
         }
@@ -129,6 +127,19 @@
     return account;
   }
 
+  protected void addUserToGroups(Account.Id id, String... groupNames) throws Exception {
+    if (groupNames != null) {
+      for (String n : groupNames) {
+        AccountGroup.NameKey k = AccountGroup.nameKey(n);
+        Optional<InternalGroup> group = groupCache.get(k);
+        if (!group.isPresent()) {
+          throw new NoSuchGroupException(n);
+        }
+        addGroupMember(group.get().getGroupUUID(), id);
+      }
+    }
+  }
+
   public TestAccount create(@Nullable String username, String group) throws Exception {
     return create(username, null, username, null, group);
   }
@@ -178,7 +189,7 @@
     return ImmutableList.copyOf(accounts.values());
   }
 
-  private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
+  protected void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
       throws IOException, NoSuchGroupException, ConfigInvalidException {
     GroupDelta groupDelta =
         GroupDelta.builder()
diff --git a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
index 88b97c7..17e0559 100644
--- a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
@@ -52,6 +52,11 @@
     countsByAccount.remove(accountId.get());
   }
 
+  public void assertReindexAtLeastOnceOf(Account.Id accountId) {
+    assertThat(countsByAccount.asMap().getOrDefault(accountId.get(), 0L)).isAtLeast(1);
+    countsByAccount.remove(accountId.get());
+  }
+
   public void assertNoReindex() {
     assertThat(countsByAccount.asMap()).isEmpty();
   }
diff --git a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
index 5991646..98287c8 100644
--- a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
@@ -41,6 +41,10 @@
     return countsByChange.get(info._number);
   }
 
+  public long getTotalCount() {
+    return countsByChange.asMap().values().stream().reduce(0L, Long::sum);
+  }
+
   public void assertReindexOf(ChangeInfo info) {
     assertReindexOf(info, 1);
   }
diff --git a/java/com/google/gerrit/acceptance/DaemonTestRule.java b/java/com/google/gerrit/acceptance/DaemonTestRule.java
new file mode 100644
index 0000000..fc2a0cc
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/DaemonTestRule.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.rules.TestRule;
+
+/**
+ * Test rule required to run {@link AbstractDaemonTest}.
+ *
+ * <p>The Google internal implementation uses own infrastructure instead of the {@link
+ * GerritServer}.
+ */
+@UsedAt(Project.GOOGLE)
+public interface DaemonTestRule extends TestRule {
+  TestConfigRule configRule();
+
+  ServerTestRule server();
+
+  TimeSettingsTestRule timeSettingsRule();
+
+  TestRepository<InMemoryRepository> cloneProject(NameKey p, TestAccount testAccount)
+      throws Exception;
+
+  String name(String name);
+}
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 1618573..702b3b4 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -172,7 +172,8 @@
   }
 
   public void assertNoRefUpdatedEvents(String project, String branch) throws Exception {
-    getRefUpdatedEvents(project, branch, 0);
+    @SuppressWarnings("unused")
+    var unused = getRefUpdatedEvents(project, branch, 0);
   }
 
   public void assertRefUpdatedEvents(String project, String branch, String... expected)
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index f3527f0..d2051d5 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
@@ -45,6 +46,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.change.FilterIncludedIn;
 import com.google.gerrit.server.change.ReviewerSuggestion;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.ChangeMessageModifier;
@@ -87,6 +89,7 @@
   private final DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
   private final DynamicSet<GitBatchRefUpdateListener> batchRefUpdateListeners;
   private final DynamicSet<FileHistoryWebLink> fileHistoryWebLinks;
+  private final DynamicSet<FilterIncludedIn> filterIncludedIns;
   private final DynamicSet<PatchSetWebLink> patchSetWebLinks;
   private final DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks;
   private final DynamicSet<EditWebLink> editWebLinks;
@@ -133,6 +136,7 @@
       DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners,
       DynamicSet<GitBatchRefUpdateListener> batchRefUpdateListeners,
       DynamicSet<FileHistoryWebLink> fileHistoryWebLinks,
+      DynamicSet<FilterIncludedIn> filterIncludedIns,
       DynamicSet<PatchSetWebLink> patchSetWebLinks,
       DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks,
       DynamicSet<EditWebLink> editWebLinks,
@@ -174,6 +178,7 @@
     this.refUpdatedListeners = refUpdatedListeners;
     this.batchRefUpdateListeners = batchRefUpdateListeners;
     this.fileHistoryWebLinks = fileHistoryWebLinks;
+    this.filterIncludedIns = filterIncludedIns;
     this.patchSetWebLinks = patchSetWebLinks;
     this.editWebLinks = editWebLinks;
     this.fileWebLinks = fileWebLinks;
@@ -205,172 +210,219 @@
   public class Registration implements AutoCloseable {
     private final List<RegistrationHandle> registrationHandles = new ArrayList<>();
 
+    @CanIgnoreReturnValue
     public Registration add(AccountIndexedListener accountIndexedListener) {
       return add(accountIndexedListeners, accountIndexedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeIndexedListener changeIndexedListener) {
       return add(changeIndexedListeners, changeIndexedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(GroupIndexedListener groupIndexedListener) {
       return add(groupIndexedListeners, groupIndexedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ProjectIndexedListener projectIndexedListener) {
       return add(projectIndexedListeners, projectIndexedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(CommitValidationListener commitValidationListener) {
       return add(commitValidationListeners, commitValidationListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(TopicEditedListener topicEditedListener) {
       return add(topicEditedListeners, topicEditedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ExceptionHook exceptionHook) {
       return add(exceptionHooks, exceptionHook);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(PerformanceLogger performanceLogger) {
       return add(performanceLoggers, performanceLogger);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ProjectCreationValidationListener projectCreationListener) {
       return add(projectCreationValidationListeners, projectCreationListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(SubmitRule submitRule) {
       return add(submitRules, submitRule);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(SubmitRequirement submitRequirement) {
       return add(submitRequirements, submitRequirement);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeHasOperandFactory hasOperand, String exportName) {
       return add(hasOperands, hasOperand, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeIsOperandFactory isOperand, String exportName) {
       return add(isOperands, isOperand, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeMessageModifier changeMessageModifier) {
       return add(changeMessageModifiers, changeMessageModifier);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeMessageModifier changeMessageModifier, String exportName) {
       return add(changeMessageModifiers, changeMessageModifier, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ChangeETagComputation changeETagComputation) {
       return add(changeETagComputations, changeETagComputation);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ActionVisitor actionVisitor) {
       return add(actionVisitors, actionVisitor);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(DownloadScheme downloadScheme, String exportName) {
       return add(downloadSchemes, downloadScheme, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(RefOperationValidationListener refOperationValidationListener) {
       return add(refOperationValidationListeners, refOperationValidationListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(CommentAddedListener commentAddedListener) {
       return add(commentAddedListeners, commentAddedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(GitReferenceUpdatedListener refUpdatedListener) {
       return add(refUpdatedListeners, refUpdatedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(GitBatchRefUpdateListener batchRefUpdateListener) {
       return add(batchRefUpdateListeners, batchRefUpdateListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(FileHistoryWebLink fileHistoryWebLink) {
       return add(fileHistoryWebLinks, fileHistoryWebLink);
     }
 
+    @CanIgnoreReturnValue
+    public Registration add(FilterIncludedIn filterIncludedIn) {
+      return add(filterIncludedIns, filterIncludedIn);
+    }
+
+    @CanIgnoreReturnValue
     public Registration add(PatchSetWebLink patchSetWebLink) {
       return add(patchSetWebLinks, patchSetWebLink);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ResolveConflictsWebLink resolveConflictsWebLink) {
       return add(resolveConflictsWebLinks, resolveConflictsWebLink);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(EditWebLink editWebLink) {
       return add(editWebLinks, editWebLink);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(FileWebLink fileWebLink) {
       return add(fileWebLinks, fileWebLink);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(RevisionCreatedListener revisionCreatedListener) {
       return add(revisionCreatedListeners, revisionCreatedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(GroupBackend groupBackend) {
       return add(groupBackends, groupBackend);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(
         AccountActivationValidationListener accountActivationValidationListener) {
       return add(accountActivationValidationListeners, accountActivationValidationListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(AccountActivationListener accountDeactivatedListener) {
       return add(accountActivationListeners, accountDeactivatedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(OnSubmitValidationListener onSubmitValidationListener) {
       return add(onSubmitValidationListeners, onSubmitValidationListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(WorkInProgressStateChangedListener workInProgressStateChangedListener) {
       return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(AttentionSetListener attentionSetListener) {
       return add(attentionSetListeners, attentionSetListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
       return add(capabilityDefinitions, capabilityDefinition, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(
         PluginProjectPermissionDefinition pluginProjectPermissionDefinition, String exportName) {
       return add(pluginProjectPermissionDefinitions, pluginProjectPermissionDefinition, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ProjectConfigEntry pluginConfigEntry, String exportName) {
       return add(pluginConfigEntries, pluginConfigEntry, exportName);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(PluginPushOption pluginPushOption) {
       return add(pluginPushOptions, pluginPushOption);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(OnPostReview onPostReview) {
       return add(onPostReviews, onPostReview);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ReviewerAddedListener reviewerAddedListener) {
       return add(reviewerAddedListeners, reviewerAddedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ReviewerDeletedListener reviewerDeletedListener) {
       return add(reviewerDeletedListeners, reviewerDeletedListener);
     }
 
+    @CanIgnoreReturnValue
     public Registration add(ReviewerSuggestion reviewerSuggestion, String exportName) {
       return add(reviewerSuggestions, reviewerSuggestion, exportName);
     }
diff --git a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
index a1c28b9..433c149 100644
--- a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
+++ b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.httpd.GitOverHttpServlet;
 import com.google.gerrit.server.AuditEvent;
@@ -77,6 +78,7 @@
     }
   }
 
+  @CanIgnoreReturnValue
   public ImmutableList<HttpAuditEvent> drainHttpAuditEvents() throws Exception {
     // Assumes that all HttpAuditEvents are produced by GitOverHttpServlet.
     int expectedSize = Ints.checkedCast(httpMetrics.getRequestsStarted() - drainedSoFar.get());
diff --git a/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java b/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java
new file mode 100644
index 0000000..4f4d67a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.entities.Project.NameKey;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.rules.RuleChain;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/** Implements {@link DaemonTestRule} using {@link GerritServer}. */
+public class GerritServerDaemonTestRule implements DaemonTestRule {
+  public static GerritServerDaemonTestRule create(AbstractDaemonTest test) {
+    TestConfigRule configRule = new TestConfigRule(AbstractDaemonTest.temporaryFolder, test);
+    GerritServerTestRule server =
+        new GerritServerTestRule(
+            configRule,
+            AbstractDaemonTest.temporaryFolder,
+            () -> test.createModule(),
+            () -> test.createAuditModule(),
+            () -> test.createSshModule());
+    TimeSettingsTestRule timeSettingsRule = new TimeSettingsTestRule(configRule);
+    return new GerritServerDaemonTestRule(test, configRule, server, timeSettingsRule);
+  }
+
+  private final RuleChain ruleChain;
+  private final TestConfigRule configRule;
+  private final ServerTestRule server;
+
+  private final TimeSettingsTestRule timeSettingsRule;
+
+  private final AbstractDaemonTest test;
+
+  private GerritServerDaemonTestRule(
+      AbstractDaemonTest test,
+      TestConfigRule configRule,
+      ServerTestRule server,
+      TimeSettingsTestRule timeSettingsRule) {
+    this.configRule = configRule;
+    this.server = server;
+    this.timeSettingsRule = timeSettingsRule;
+    this.test = test;
+    // Set the clock step as almost the last step, so that the test setup isn't consuming any
+    // timestamps after the
+    // clock has been set.
+    ruleChain =
+        RuleChain.outerRule(configRule)
+            .around(server)
+            .around(test.testRunner)
+            .around(timeSettingsRule);
+  }
+
+  @Override
+  public TestConfigRule configRule() {
+    return configRule;
+  }
+
+  @Override
+  public ServerTestRule server() {
+    return server;
+  }
+
+  @Override
+  public TimeSettingsTestRule timeSettingsRule() {
+    return timeSettingsRule;
+  }
+
+  @Override
+  public TestRepository<InMemoryRepository> cloneProject(NameKey p, TestAccount testAccount)
+      throws Exception {
+    return GitUtil.cloneProject(p, test.registerRepoConnection(p, testAccount));
+  }
+
+  @Override
+  public String name(String name) {
+    return test.resourcePrefix + name;
+  }
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return ruleChain.apply(statement, description);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServerRestSession.java b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
new file mode 100644
index 0000000..c2c77fe
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.net.HttpHeaders.ACCEPT;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.gerrit.json.OutputFormat.JSON_COMPACT;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.RawInput;
+import java.io.IOException;
+import org.apache.http.Header;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
+
+/** Sends requests to {@link GerritServer} as a specified user. */
+public class GerritServerRestSession extends HttpSession implements RestSession {
+
+  public GerritServerRestSession(GerritServer server, @Nullable TestAccount account) {
+    super(server, account);
+  }
+
+  @Override
+  public RestResponse get(String endPoint) throws IOException {
+    return getWithHeaders(endPoint);
+  }
+
+  @Override
+  public RestResponse getJsonAccept(String endPoint) throws IOException {
+    return getWithHeaders(endPoint, new BasicHeader(ACCEPT, "application/json"));
+  }
+
+  @Override
+  public RestResponse getWithHeaders(String endPoint, Header... headers) throws IOException {
+    Request get = Request.Get(getUrl(endPoint));
+    if (headers != null) {
+      get.setHeaders(headers);
+    }
+    return execute(get);
+  }
+
+  @Override
+  public RestResponse head(String endPoint) throws IOException {
+    return execute(Request.Head(getUrl(endPoint)));
+  }
+
+  @Override
+  public RestResponse put(String endPoint) throws IOException {
+    return put(endPoint, /* content = */ null);
+  }
+
+  @Override
+  public RestResponse put(String endPoint, Object content) throws IOException {
+    return putWithHeaders(endPoint, content);
+  }
+
+  @Override
+  public RestResponse putWithHeaders(String endPoint, Header... headers) throws IOException {
+    return putWithHeaders(endPoint, /* content= */ null, headers);
+  }
+
+  @Override
+  public RestResponse putWithHeaders(String endPoint, Object content, Header... headers)
+      throws IOException {
+    Request put = Request.Put(getUrl(endPoint));
+    if (headers != null) {
+      put.setHeaders(headers);
+    }
+    if (content != null) {
+      addContentToRequest(put, content);
+    }
+    return execute(put);
+  }
+
+  @Override
+  public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
+    requireNonNull(stream);
+    Request put = Request.Put(getUrl(endPoint));
+    put.addHeader(new BasicHeader(CONTENT_TYPE, stream.getContentType()));
+    put.body(
+        new BufferedHttpEntity(
+            new InputStreamEntity(stream.getInputStream(), stream.getContentLength())));
+    return execute(put);
+  }
+
+  @Override
+  public RestResponse post(String endPoint) throws IOException {
+    return post(endPoint, /* content = */ null);
+  }
+
+  @Override
+  public RestResponse post(String endPoint, Object content) throws IOException {
+    return postWithHeaders(endPoint, content);
+  }
+
+  @Override
+  public RestResponse postWithHeaders(String endPoint, Object content, Header... headers)
+      throws IOException {
+    Request post = Request.Post(getUrl(endPoint));
+    if (headers != null) {
+      post.setHeaders(headers);
+    }
+    if (content != null) {
+      addContentToRequest(post, content);
+    }
+    return execute(post);
+  }
+
+  private static void addContentToRequest(Request request, Object content) {
+    request.addHeader(new BasicHeader(CONTENT_TYPE, "application/json"));
+    request.body(new StringEntity(JSON_COMPACT.newGson().toJson(content), UTF_8));
+  }
+
+  @Override
+  public RestResponse delete(String endPoint) throws IOException {
+    return execute(Request.Delete(getUrl(endPoint)));
+  }
+
+  @Override
+  public RestResponse deleteWithHeaders(String endPoint, Header... headers) throws IOException {
+    Request delete = Request.Delete(getUrl(endPoint));
+    if (headers != null) {
+      delete.setHeaders(headers);
+    }
+    return execute(delete);
+  }
+
+  private String getUrl(String endPoint) {
+    return url + (account != null ? "/a" : "") + endPoint;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServerTestRule.java b/java/com/google/gerrit/acceptance/GerritServerTestRule.java
new file mode 100644
index 0000000..d3bc008
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerTestRule.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.testing.SshMode;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class GerritServerTestRule implements ServerTestRule {
+  /**
+   * Test methods without special annotations will use a common server for efficiency reasons. The
+   * server is torn down after the test class is done or when the config is changed.
+   */
+  private static GerritServer commonServer;
+
+  private static Description firstTest;
+
+  private final TemporaryFolder temporaryFolder;
+  @Nullable private final Supplier<Module> testSysModule;
+  @Nullable private final Supplier<Module> testAuditModule;
+  @Nullable private final Supplier<Module> testSshModule;
+  private final TestConfigRule config;
+
+  @Inject private TestSshKeys sshKeys;
+  @Inject @Nullable @TestSshServerAddress private InetSocketAddress sshAddress;
+
+  @Inject private AccountOperations accountOperations;
+
+  private boolean sshInitialized;
+
+  private final HashMap<RequestContext, SshSession> sshSessionByContext = new HashMap<>();
+
+  public GerritServer server;
+
+  public GerritServerTestRule(
+      TestConfigRule config,
+      TemporaryFolder temporaryFolder,
+      @Nullable Supplier<Module> testSysModule,
+      @Nullable Supplier<Module> testAuditModule,
+      @Nullable Supplier<Module> testSshModule) {
+    this.config = config;
+    this.testSysModule = testSysModule;
+    this.testAuditModule = testAuditModule;
+    this.testSshModule = testSshModule;
+    this.temporaryFolder = temporaryFolder;
+  }
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        if (firstTest == null) {
+          firstTest = description;
+        }
+        if (config.testRequiresSsh()) {
+          // If the test uses ssh, we use assume() to make sure ssh is enabled on
+          // the test suite. JUnit will skip tests annotated with @UseSsh if we
+          // disable them using the command line flag.
+          assume().that(SshMode.useSsh()).isTrue();
+        }
+        statement.evaluate();
+        afterTest();
+      }
+    };
+  }
+
+  public void afterTest() throws Exception {
+    closeSsh();
+    if (server != commonServer) {
+      server.close();
+      server = null;
+    }
+  }
+
+  @Override
+  public void initServer() throws Exception {
+
+    if (config.classDescription().equals(config.methodDescription())
+        && !config.classDescription().sandboxed()
+        && !config.methodDescription().sandboxed()) {
+      if (commonServer == null) {
+        commonServer =
+            GerritServer.initAndStart(
+                temporaryFolder,
+                config.classDescription(),
+                config.baseConfig(),
+                testSysModule.get(),
+                testAuditModule.get(),
+                testSshModule.get());
+      }
+      server = commonServer;
+    } else {
+      server =
+          GerritServer.initAndStart(
+              temporaryFolder,
+              config.methodDescription(),
+              config.baseConfig(),
+              testSysModule.get(),
+              testAuditModule.get(),
+              testSshModule.get());
+    }
+    getTestInjector().injectMembers(this);
+  }
+
+  @Override
+  public void initSsh() throws Exception {
+    if (config.testRequiresSsh() && SshMode.useSsh()) {
+      checkState(!sshInitialized, "The ssh has been alread initialized. Call closeSsh first.");
+      // Create Ssh sessions
+      SshSessionFactory.initSsh();
+      sshInitialized = true;
+    }
+  }
+
+  @Override
+  public boolean sshInitialized() {
+    return sshInitialized;
+  }
+
+  @Override
+  public SshSession getOrCreateSshSessionForContext(RequestContext ctx) {
+    checkState(
+        config.testRequiresSsh(),
+        "The test or the test class must be annotated with @UseSsh to use this method.");
+    return sshSessionByContext.computeIfAbsent(
+        ctx,
+        (c) ->
+            SshSessionFactory.createSession(
+                sshKeys,
+                sshAddress,
+                accountOperations.account(ctx.getUser().getAccountId()).get()));
+  }
+
+  /**
+   * This method is only required for some tests and is not a part of interface.
+   *
+   * <p>After restarting the server with this method, the caller can still get exit value of the
+   * last command executed before restarting (using non-closed sessions). This is used in
+   * SshDaemonIT tests.
+   */
+  public void restartKeepSessionOpen() throws Exception {
+    checkState(
+        server != commonServer,
+        "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+    server = GerritServer.restart(server, testSysModule.get(), testSshModule.get());
+    getTestInjector().injectMembers(this);
+  }
+
+  @Override
+  public void restart() throws Exception {
+    checkState(
+        server != commonServer,
+        "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+    closeSsh();
+    server = GerritServer.restart(server, testSysModule.get(), testSshModule.get());
+    getTestInjector().injectMembers(this);
+    initSsh();
+  }
+
+  @Override
+  public void restartAsSlave() throws Exception {
+    checkState(
+        server != commonServer,
+        "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+    closeSsh();
+    server = GerritServer.restartAsSlave(server);
+    getTestInjector().injectMembers(this);
+    initSsh();
+  }
+
+  protected void closeSsh() {
+    sshSessionByContext.values().forEach(SshSession::close);
+    sshSessionByContext.clear();
+    sshInitialized = false;
+  }
+
+  @Override
+  public Injector getTestInjector() {
+    return server.getTestInjector();
+  }
+
+  @Override
+  public Optional<Injector> getHttpdInjector() {
+    return server.getHttpdInjector();
+  }
+
+  @Override
+  public RestSession createRestSession(@Nullable TestAccount account) {
+    return new GerritServerRestSession(server, account);
+  }
+
+  @Nullable
+  @Override
+  public ProjectResetter createProjectResetter(
+      BiFunction<AllProjectsName, AllUsersName, ProjectResetter.Config> resetConfigSupplier)
+      throws Exception {
+    // Only commonServer can be shared between tests and should be restored after each
+    // test. It can be that the commonServer is started, but a test actually don't use
+    // it and instead the test uses a separate server instance.
+    // In this case, the separate server is stopped after each test and so doesn't require
+    // cleanup (for simplicity, the commonServer is always cleaned up even if it is not
+    // used in a test).
+    if (commonServer == null) {
+      return null;
+    }
+    Injector testInjector = commonServer.testInjector;
+    ProjectResetter.Config config =
+        requireNonNull(
+            resetConfigSupplier.apply(
+                testInjector.getInstance(AllProjectsName.class),
+                testInjector.getInstance(AllUsersName.class)));
+    ProjectResetter.Builder.Factory projectResetterFactory =
+        testInjector.getInstance(ProjectResetter.Builder.Factory.class);
+    return projectResetterFactory != null ? projectResetterFactory.builder().build(config) : null;
+  }
+
+  @Override
+  public boolean isReplica() {
+    return server.isReplica();
+  }
+
+  @Override
+  public Optional<InetSocketAddress> getHttpAddress() {
+    return server.getHttpAddress();
+  }
+
+  @Override
+  public String getGitUrl() {
+    return server.getUrl();
+  }
+
+  @Override
+  public boolean isUsernameSupported() {
+    return true;
+  }
+
+  public static void afterConfigChanged() {
+    if (commonServer != null) {
+      try {
+        commonServer.close();
+      } catch (Exception e) {
+        throw new AssertionError(
+            "Error stopping common server in "
+                + (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
+            e);
+      } finally {
+        commonServer = null;
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index 94d329d..335e97c 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.entities.Project;
 import java.io.IOException;
@@ -98,6 +99,7 @@
     return testRepo;
   }
 
+  @CanIgnoreReturnValue
   public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
       throws GitAPIException {
     TagCommand cmd =
@@ -105,6 +107,7 @@
     return cmd.call();
   }
 
+  @CanIgnoreReturnValue
   public static Ref updateAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
       throws GitAPIException {
     TagCommand tc = testRepo.git().tag().setName(name);
@@ -117,21 +120,25 @@
     fetch.call();
   }
 
+  @CanIgnoreReturnValue
   public static PushResult pushHead(TestRepository<?> testRepo, String ref) throws GitAPIException {
     return pushHead(testRepo, ref, false);
   }
 
+  @CanIgnoreReturnValue
   public static PushResult pushHead(TestRepository<?> testRepo, String ref, boolean pushTags)
       throws GitAPIException {
     return pushHead(testRepo, ref, pushTags, false);
   }
 
+  @CanIgnoreReturnValue
   public static PushResult pushHead(
       TestRepository<?> testRepo, String ref, boolean pushTags, boolean force)
       throws GitAPIException {
     return pushOne(testRepo, "HEAD", ref, pushTags, force, null);
   }
 
+  @CanIgnoreReturnValue
   public static PushResult pushHead(
       TestRepository<?> testRepo,
       String ref,
@@ -142,11 +149,13 @@
     return pushOne(testRepo, "HEAD", ref, pushTags, force, pushOptions);
   }
 
+  @CanIgnoreReturnValue
   public static PushResult deleteRef(TestRepository<?> testRepo, String ref)
       throws GitAPIException {
     return pushOne(testRepo, "", ref, false, true, null);
   }
 
+  @CanIgnoreReturnValue
   public static PushResult pushOne(
       TestRepository<?> testRepo,
       String source,
diff --git a/java/com/google/gerrit/acceptance/HttpSession.java b/java/com/google/gerrit/acceptance/HttpSession.java
index 833c53b..ef14a3d 100644
--- a/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/java/com/google/gerrit/acceptance/HttpSession.java
@@ -25,7 +25,7 @@
 import org.apache.http.impl.client.HttpClientBuilder;
 
 public class HttpSession {
-  protected TestAccount account;
+  protected @Nullable TestAccount account;
   protected final String url;
   private final Executor executor;
 
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 17ce595..abcc108 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -230,7 +230,9 @@
       // have an easy way to run code when this instance is done being used.
       // Each operation is run in its own thread, so we don't need to recover
       // its original context anyway.
-      threadContext.setContext(req);
+      @SuppressWarnings("unused")
+      var unused = threadContext.setContext(req);
+
       current.set(req);
 
       PermissionBackend.ForProject perm = permissionBackend.currentUser().project(req.project);
@@ -300,7 +302,9 @@
       // have an easy way to run code when this instance is done being used.
       // Each operation is run in its own thread, so we don't need to recover
       // its original context anyway.
-      threadContext.setContext(req);
+      @SuppressWarnings("unused")
+      var unused = threadContext.setContext(req);
+
       current.set(req);
       try {
         permissionBackend
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index c8ab1a9..c71641b 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -20,6 +20,8 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
@@ -137,19 +139,37 @@
   }
 
   public static class Config {
-    private final Multimap<Project.NameKey, String> refsByProject;
+    private final ImmutableMultimap<Project.NameKey, String> refsByProject;
 
-    public Config() {
-      this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
+    private Config(ImmutableMultimap<Project.NameKey, String> refsByProject) {
+      this.refsByProject = refsByProject;
     }
 
-    public Config reset(Project.NameKey project, String... refPatterns) {
-      List<String> refPatternList = Arrays.asList(refPatterns);
-      if (refPatternList.isEmpty()) {
-        refPatternList = ImmutableList.of(RefNames.REFS + "*");
+    public Builder toBuilder() {
+      Builder builder = new Builder();
+      builder.refsByProject.putAll(refsByProject);
+      return builder;
+    }
+
+    public static class Builder {
+      private final ListMultimap<Project.NameKey, String> refsByProject;
+
+      public Builder() {
+        this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
       }
-      refsByProject.putAll(project, refPatternList);
-      return this;
+
+      public Builder reset(Project.NameKey project, String... refPatterns) {
+        List<String> refPatternList = Arrays.asList(refPatterns);
+        if (refPatternList.isEmpty()) {
+          refPatternList = ImmutableList.of(RefNames.REFS + "*");
+        }
+        refsByProject.putAll(project, refPatternList);
+        return this;
+      }
+
+      public Config build() {
+        return new Config(ImmutableMultimap.copyOf(refsByProject));
+      }
     }
   }
 
@@ -166,12 +186,12 @@
   private final Multimap<Project.NameKey, String> refsPatternByProject;
 
   // State to which to reset to.
-  private final Multimap<Project.NameKey, RefState> savedRefStatesByProject;
+  private final ListMultimap<Project.NameKey, RefState> savedRefStatesByProject;
 
   // Results of the resetting
-  private Multimap<Project.NameKey, String> keptRefsByProject;
-  private Multimap<Project.NameKey, String> restoredRefsByProject;
-  private Multimap<Project.NameKey, String> deletedRefsByProject;
+  private ListMultimap<Project.NameKey, String> keptRefsByProject;
+  private ListMultimap<Project.NameKey, String> restoredRefsByProject;
+  private ListMultimap<Project.NameKey, String> deletedRefsByProject;
 
   private ProjectResetter(
       GitRepositoryManager repoManager,
@@ -212,13 +232,13 @@
   }
 
   /** Read the states of all matching refs. */
-  private Multimap<Project.NameKey, RefState> readRefStates() throws IOException {
-    Multimap<Project.NameKey, RefState> refStatesByProject =
+  private ListMultimap<Project.NameKey, RefState> readRefStates() throws IOException {
+    ListMultimap<Project.NameKey, RefState> refStatesByProject =
         MultimapBuilder.hashKeys().arrayListValues().build();
     for (Map.Entry<Project.NameKey, Collection<String>> e :
         refsPatternByProject.asMap().entrySet()) {
       try (Repository repo = repoManager.openRepository(e.getKey())) {
-        Collection<Ref> refs = repo.getRefDatabase().getRefs();
+        List<Ref> refs = repo.getRefDatabase().getRefs();
         for (String refPattern : e.getValue()) {
           RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern);
           for (Ref ref : refs) {
@@ -262,7 +282,7 @@
     for (Map.Entry<Project.NameKey, Collection<String>> e :
         refsPatternByProject.asMap().entrySet()) {
       try (Repository repo = repoManager.openRepository(e.getKey())) {
-        Collection<Ref> nonRestoredRefs =
+        Set<Ref> nonRestoredRefs =
             repo.getRefDatabase().getRefs().stream()
                 .filter(
                     r ->
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index a61fa46..2d53533 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -95,6 +96,9 @@
     PushOneCommit create(PersonIdent i, TestRepository<?> testRepo);
 
     PushOneCommit create(
+        PersonIdent i, TestRepository<?> testRepo, boolean insertChangeIdIfNotExist);
+
+    PushOneCommit create(
         PersonIdent i, TestRepository<?> testRepo, @Assisted("changeId") String changeId);
 
     PushOneCommit create(
@@ -172,7 +176,7 @@
   private final TestRepository<?>.CommitBuilder commitBuilder;
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo)
@@ -181,7 +185,24 @@
   }
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
+      Result.Factory pushResultFactory,
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
+      @Assisted boolean insertChangeIdIfNotExist)
+      throws Exception {
+    this(
+        pushResultFactory,
+        i,
+        testRepo,
+        SUBJECT,
+        ImmutableMap.of(FILE_NAME, FILE_CONTENT),
+        /* changeId= */ null,
+        insertChangeIdIfNotExist);
+  }
+
+  @AssistedInject
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -191,7 +212,7 @@
   }
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -203,18 +224,19 @@
   }
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted String subject,
       @Assisted Map<String, String> files)
       throws Exception {
-    this(pushResultFactory, i, testRepo, subject, files, null);
+    this(
+        pushResultFactory, i, testRepo, subject, files, null, /* insertChangeIdIfNotExist= */ true);
   }
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -223,11 +245,18 @@
       @Assisted("content") String content,
       @Nullable @Assisted("changeId") String changeId)
       throws Exception {
-    this(pushResultFactory, i, testRepo, subject, ImmutableMap.of(fileName, content), changeId);
+    this(
+        pushResultFactory,
+        i,
+        testRepo,
+        subject,
+        ImmutableMap.of(fileName, content),
+        changeId,
+        /* insertChangeIdIfNotExist= */ true);
   }
 
   @AssistedInject
-  PushOneCommit(
+  public PushOneCommit(
       Result.Factory pushResultFactory,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -235,6 +264,26 @@
       @Assisted Map<String, String> files,
       @Nullable @Assisted("changeId") String changeId)
       throws Exception {
+    this(
+        pushResultFactory,
+        i,
+        testRepo,
+        subject,
+        files,
+        changeId,
+        /* insertChangeIdIfNotExist= */ true);
+  }
+
+  @AssistedInject
+  public PushOneCommit(
+      Result.Factory pushResultFactory,
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
+      @Assisted("subject") String subject,
+      @Assisted Map<String, String> files,
+      @Nullable @Assisted("changeId") String changeId,
+      @Assisted boolean insertChangeIdIfNotExist)
+      throws Exception {
     this.testRepo = testRepo;
     this.subject = subject;
     this.files = files;
@@ -242,16 +291,24 @@
     this.pushResultFactory = pushResultFactory;
     if (changeId != null) {
       commitBuilder = testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
-    } else {
+    } else if (insertChangeIdIfNotExist) {
       if (subject.contains("\nChange-Id: ")) {
         commitBuilder = testRepo.amendRef("HEAD");
       } else {
         commitBuilder = testRepo.branch("HEAD").commit().insertChangeId(nextChangeId());
       }
+    } else {
+      commitBuilder = testRepo.amendRef("HEAD");
     }
     commitBuilder.message(subject).author(i).committer(new PersonIdent(i, testRepo.getDate()));
   }
 
+  @UsedAt(Project.GOOGLE)
+  protected TestRepository<?> testRepository() {
+    return testRepo;
+  }
+
+  @CanIgnoreReturnValue
   public PushOneCommit setParents(List<RevCommit> parents) throws Exception {
     commitBuilder.noParents();
     for (RevCommit p : parents) {
@@ -266,17 +323,20 @@
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit setParent(RevCommit parent) throws Exception {
     commitBuilder.noParents();
     commitBuilder.parent(parent);
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit noParent() {
     commitBuilder.noParents();
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit addFile(String path, String content, int fileMode) throws Exception {
     RevBlob blobId = testRepo.blob(content);
     commitBuilder.edit(
@@ -290,6 +350,7 @@
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit addSymlink(String path, String target) throws Exception {
     RevBlob blobId = testRepo.blob(target);
     commitBuilder.edit(
@@ -303,6 +364,7 @@
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit addGitSubmodule(String modulePath, ObjectId commitId) {
     commitBuilder.edit(
         new PathEdit(modulePath) {
@@ -315,11 +377,13 @@
     return this;
   }
 
+  @CanIgnoreReturnValue
   public PushOneCommit rmFile(String filename) {
     commitBuilder.rm(filename);
     return this;
   }
 
+  @CanIgnoreReturnValue
   public Result to(String ref) throws Exception {
     for (Map.Entry<String, String> e : files.entrySet()) {
       commitBuilder.add(e.getKey(), e.getValue());
@@ -327,6 +391,7 @@
     return execute(ref);
   }
 
+  @CanIgnoreReturnValue
   public Result rm(String ref) throws Exception {
     for (String fileName : files.keySet()) {
       commitBuilder.rm(fileName);
@@ -334,10 +399,11 @@
     return execute(ref);
   }
 
+  @CanIgnoreReturnValue
   public Result execute(String ref) throws Exception {
     RevCommit c = commitBuilder.create();
     if (changeId == null) {
-      changeId = GitUtil.getChangeId(testRepo, c).get();
+      changeId = GitUtil.getChangeId(testRepo, c).orElse(null);
     }
     if (tag != null) {
       TagCommand tagCommand = testRepo.git().tag().setName(tag.name);
@@ -387,7 +453,7 @@
       Result create(
           @Assisted("ref") String ref,
           @Assisted("subject") String subject,
-          @Assisted("changeId") String changeId,
+          @Nullable @Assisted("changeId") String changeId,
           @Nullable PushResult resSubj,
           @Nullable RevCommit commit,
           @Nullable List<String> pushOptions);
@@ -413,7 +479,7 @@
         Provider<InternalChangeQuery> queryProvider,
         @Assisted("ref") String ref,
         @Assisted("subject") String subject,
-        @Assisted("changeId") String changeId,
+        @Assisted("changeId") @Nullable String changeId,
         @Assisted @Nullable PushResult resSubj,
         @Assisted @Nullable RevCommit commit,
         @Assisted @Nullable List<String> pushOptions) {
@@ -473,7 +539,7 @@
 
     private void assertReviewers(
         Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) {
-      Iterable<Account.Id> actualIds =
+      ImmutableSet<Account.Id> actualIds =
           approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state);
       assertThat(actualIds)
           .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers)));
diff --git a/java/com/google/gerrit/acceptance/RestResponse.java b/java/com/google/gerrit/acceptance/RestResponse.java
index a045d80..a9c14aa 100644
--- a/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/java/com/google/gerrit/acceptance/RestResponse.java
@@ -30,6 +30,8 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -37,7 +39,8 @@
 
 public class RestResponse extends HttpResponse {
 
-  RestResponse(org.apache.http.HttpResponse response) {
+  @UsedAt(Project.GOOGLE)
+  public RestResponse(org.apache.http.HttpResponse response) {
     super(response);
   }
 
diff --git a/java/com/google/gerrit/acceptance/RestSession.java b/java/com/google/gerrit/acceptance/RestSession.java
index 342cbd0..0865e31 100644
--- a/java/com/google/gerrit/acceptance/RestSession.java
+++ b/java/com/google/gerrit/acceptance/RestSession.java
@@ -11,123 +11,45 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-
 package com.google.gerrit.acceptance;
 
-import static com.google.common.net.HttpHeaders.ACCEPT;
-import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
-import static com.google.gerrit.json.OutputFormat.JSON_COMPACT;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.extensions.restapi.RawInput;
-import java.io.IOException;
 import org.apache.http.Header;
 import org.apache.http.client.fluent.Request;
-import org.apache.http.entity.BufferedHttpEntity;
-import org.apache.http.entity.InputStreamEntity;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.message.BasicHeader;
 
-public class RestSession extends HttpSession {
+/** Makes rest requests to gerrit backend. */
+@UsedAt(UsedAt.Project.GOOGLE) // Google has own implementation of this interface in tests.
+public interface RestSession {
+  String url();
 
-  public RestSession(GerritServer server, @Nullable TestAccount account) {
-    super(server, account);
-  }
+  RestResponse execute(Request request) throws Exception;
 
-  public RestResponse get(String endPoint) throws IOException {
-    return getWithHeaders(endPoint);
-  }
+  RestResponse get(String endPoint) throws Exception;
 
-  public RestResponse getJsonAccept(String endPoint) throws IOException {
-    return getWithHeaders(endPoint, new BasicHeader(ACCEPT, "application/json"));
-  }
+  RestResponse getJsonAccept(String endPoint) throws Exception;
 
-  public RestResponse getWithHeaders(String endPoint, Header... headers) throws IOException {
-    Request get = Request.Get(getUrl(endPoint));
-    if (headers != null) {
-      get.setHeaders(headers);
-    }
-    return execute(get);
-  }
+  RestResponse getWithHeaders(String endPoint, Header... headers) throws Exception;
 
-  public RestResponse head(String endPoint) throws IOException {
-    return execute(Request.Head(getUrl(endPoint)));
-  }
+  RestResponse head(String endPoint) throws Exception;
 
-  public RestResponse put(String endPoint) throws IOException {
-    return put(endPoint, /* content = */ null);
-  }
+  RestResponse put(String endPoint) throws Exception;
 
-  public RestResponse put(String endPoint, Object content) throws IOException {
-    return putWithHeaders(endPoint, content);
-  }
+  RestResponse put(String endPoint, Object content) throws Exception;
 
-  public RestResponse putWithHeaders(String endPoint, Header... headers) throws IOException {
-    return putWithHeaders(endPoint, /* content= */ null, headers);
-  }
+  RestResponse putWithHeaders(String endPoint, Header... headers) throws Exception;
 
-  public RestResponse putWithHeaders(String endPoint, Object content, Header... headers)
-      throws IOException {
-    Request put = Request.Put(getUrl(endPoint));
-    if (headers != null) {
-      put.setHeaders(headers);
-    }
-    if (content != null) {
-      addContentToRequest(put, content);
-    }
-    return execute(put);
-  }
+  RestResponse putWithHeaders(String endPoint, Object content, Header... headers) throws Exception;
 
-  public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
-    requireNonNull(stream);
-    Request put = Request.Put(getUrl(endPoint));
-    put.addHeader(new BasicHeader(CONTENT_TYPE, stream.getContentType()));
-    put.body(
-        new BufferedHttpEntity(
-            new InputStreamEntity(stream.getInputStream(), stream.getContentLength())));
-    return execute(put);
-  }
+  RestResponse putRaw(String endPoint, RawInput stream) throws Exception;
 
-  public RestResponse post(String endPoint) throws IOException {
-    return post(endPoint, /* content = */ null);
-  }
+  RestResponse post(String endPoint) throws Exception;
 
-  public RestResponse post(String endPoint, Object content) throws IOException {
-    return postWithHeaders(endPoint, content);
-  }
+  RestResponse post(String endPoint, Object content) throws Exception;
 
-  public RestResponse postWithHeaders(String endPoint, Object content, Header... headers)
-      throws IOException {
-    Request post = Request.Post(getUrl(endPoint));
-    if (headers != null) {
-      post.setHeaders(headers);
-    }
-    if (content != null) {
-      addContentToRequest(post, content);
-    }
-    return execute(post);
-  }
+  RestResponse postWithHeaders(String endPoint, Object content, Header... headers) throws Exception;
 
-  private static void addContentToRequest(Request request, Object content) {
-    request.addHeader(new BasicHeader(CONTENT_TYPE, "application/json"));
-    request.body(new StringEntity(JSON_COMPACT.newGson().toJson(content), UTF_8));
-  }
+  RestResponse delete(String endPoint) throws Exception;
 
-  public RestResponse delete(String endPoint) throws IOException {
-    return execute(Request.Delete(getUrl(endPoint)));
-  }
-
-  public RestResponse deleteWithHeaders(String endPoint, Header... headers) throws IOException {
-    Request delete = Request.Delete(getUrl(endPoint));
-    if (headers != null) {
-      delete.setHeaders(headers);
-    }
-    return execute(delete);
-  }
-
-  private String getUrl(String endPoint) {
-    return url + (account != null ? "/a" : "") + endPoint;
-  }
+  RestResponse deleteWithHeaders(String endPoint, Header... headers) throws Exception;
 }
diff --git a/java/com/google/gerrit/acceptance/ServerTestRule.java b/java/com/google/gerrit/acceptance/ServerTestRule.java
new file mode 100644
index 0000000..a057a6e
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ServerTestRule.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.acceptance.ProjectResetter.Config;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.inject.Injector;
+import java.net.InetSocketAddress;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import org.junit.rules.TestRule;
+
+public interface ServerTestRule extends TestRule {
+  /**
+   * Initialize a server.
+   *
+   * <p>All other methods must be called after this method is executed.
+   */
+  void initServer() throws Exception;
+
+  @Nullable
+  ProjectResetter createProjectResetter(
+      BiFunction<AllProjectsName, AllUsersName, Config> resetConfigSupplier) throws Exception;
+
+  Injector getTestInjector();
+
+  Optional<Injector> getHttpdInjector();
+
+  /**
+   * Initializes Ssh if a test requires it.
+   *
+   * <p>The method shouldn't throw an exception if the test doesn't require Ssh. If the test
+   * requires ssh and ssh is not supported (e.g. in internal google tests) the method throws {@link
+   * UnsupportedOperationException}.
+   */
+  void initSsh() throws Exception;
+
+  /**
+   * Restart backend as a replica and re-init Ssh if a test requires ssh.
+   *
+   * <p>The method throws {@link UnsupportedOperationException} if restarting is not supported (e.g.
+   * in internal google tests).
+   */
+  void restartAsSlave() throws Exception;
+
+  /**
+   * Restart backend as a primary and re-init Ssh if a test requires ssh.
+   *
+   * <p>The method throws {@link UnsupportedOperationException} if restarting is not supported (e.g.
+   * in internal google tests).
+   */
+  void restart() throws Exception;
+
+  /**
+   * Creates {@link RestSession} which sends all requests as a specified account.
+   *
+   * <p>For sending anonymous requests pass null as the {@code account}.
+   */
+  RestSession createRestSession(@Nullable TestAccount account);
+
+  /** Returns true if the started server is a replica. */
+  boolean isReplica();
+
+  /** Returns address to be used for http requests (if present). */
+  Optional<InetSocketAddress> getHttpAddress();
+
+  /**
+   * Gets or creates a session associated with the given context.
+   *
+   * <p>The method throws {@link UnsupportedOperationException} if ssh is not supported (e.g. in
+   * internal google tests). The method must be called only if a test or class is annotated with the
+   * UseSsh annotation.
+   */
+  SshSession getOrCreateSshSessionForContext(RequestContext ctx);
+
+  /** Returns url to be used for git operations. */
+  String getGitUrl();
+
+  /** Returns true if ssh has been initialized. */
+  boolean sshInitialized();
+
+  /**
+   * Returns true if username is supported.
+   *
+   * <p>If it is not supported tests must either skip username checks or use something else instead
+   * (e.g. email)
+   */
+  boolean isUsernameSupported();
+}
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 23bfd0b..ac1a73d 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import java.io.Reader;
@@ -38,6 +39,7 @@
 
   public abstract void close();
 
+  @CanIgnoreReturnValue
   public abstract String exec(String command) throws Exception;
 
   public abstract int execAndReturnStatus(String command) throws Exception;
diff --git a/java/com/google/gerrit/acceptance/SshSessionMina.java b/java/com/google/gerrit/acceptance/SshSessionMina.java
index 89096e4..bac4ed6 100644
--- a/java/com/google/gerrit/acceptance/SshSessionMina.java
+++ b/java/com/google/gerrit/acceptance/SshSessionMina.java
@@ -79,7 +79,8 @@
 
   @Override
   public void open() throws Exception {
-    getMinaSession();
+    @SuppressWarnings("unused")
+    var unused = getMinaSession();
   }
 
   @Override
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index dcb49a5..01e705c 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
 import com.google.common.io.ByteStreams;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -75,7 +76,8 @@
 
       try {
         // ServerContext ctor is called multiple times but the group can be only created once
-        gApi.groups().id("Group");
+        @SuppressWarnings("unused")
+        var unused = gApi.groups().id("Group");
       } catch (ResourceNotFoundException e) {
         GroupInput in = new GroupInput();
         in.members = Collections.singletonList("admin");
@@ -211,11 +213,13 @@
     runGerrit(Arrays.stream(multiArgs).flatMap(Streams::stream).toArray(String[]::new));
   }
 
+  @CanIgnoreReturnValue
   protected static String execute(
       ImmutableList<String> cmd, File dir, ImmutableMap<String, String> env) throws IOException {
     return execute(cmd, dir, env, null);
   }
 
+  @CanIgnoreReturnValue
   protected static String execute(
       ImmutableList<String> cmd,
       File dir,
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 67d8a05..85c2ddca 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -78,7 +78,7 @@
     return new PersonIdent(fullName(), email());
   }
 
-  public String getHttpUrl(GerritServer server) {
+  public String getHttpUrl(ServerTestRule server) {
     checkState(server.getHttpAddress().isPresent(), "GerritServer must have httpAddress");
     InetSocketAddress addr = server.getHttpAddress().get();
     return new URIBuilder()
diff --git a/java/com/google/gerrit/acceptance/TestConfigRule.java b/java/com/google/gerrit/acceptance/TestConfigRule.java
new file mode 100644
index 0000000..a7f051a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TestConfigRule.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
+import com.google.gerrit.acceptance.config.GerritSystemProperty;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
+import java.io.File;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class TestConfigRule implements TestRule {
+  private final TemporaryFolder temporaryFolder;
+  private final AbstractDaemonTest test;
+  private Description description;
+  private GerritServer.Description methodDescription;
+  GerritServer.Description classDescription;
+  private boolean testRequiresSsh;
+  private SystemReader oldSystemReader;
+
+  public TestConfigRule(TemporaryFolder temporaryFolder, AbstractDaemonTest test) {
+    this.temporaryFolder = temporaryFolder;
+    this.test = test;
+  }
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        setTestConfigFromDescription(description);
+        statement.evaluate();
+        clear();
+      }
+    };
+  }
+
+  private void setTestConfigFromDescription(Description description) {
+    oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
+
+    this.description = description;
+    classDescription = GerritServer.Description.forTestClass(description, test.configName);
+    methodDescription = GerritServer.Description.forTestMethod(description, test.configName);
+
+    if (methodDescription.systemProperties() != null) {
+      ConfigAnnotationParser.parse(methodDescription.systemProperties());
+    }
+
+    if (methodDescription.systemProperty() != null) {
+      ConfigAnnotationParser.parse(methodDescription.systemProperty());
+    }
+
+    test.baseConfig.unset("gerrit", null, "canonicalWebUrl");
+    test.baseConfig.unset("httpd", null, "listenUrl");
+
+    test.baseConfig.setInt("index", null, "batchThreads", -1);
+
+    testRequiresSsh = classDescription.useSshAnnotation() || methodDescription.useSshAnnotation();
+    if (!testRequiresSsh) {
+      test.baseConfig.setString("sshd", null, "listenAddress", "off");
+    }
+  }
+
+  private static SystemReader setFakeSystemReader(File tempDir) {
+    SystemReader oldSystemReader = SystemReader.getInstance();
+    SystemReader.setInstance(
+        new DelegateSystemReader(oldSystemReader) {
+          @Override
+          public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openUserConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
+          }
+        });
+    return oldSystemReader;
+  }
+
+  private void clear() {
+    if (methodDescription.systemProperties() != null) {
+      for (GerritSystemProperty sysProp : methodDescription.systemProperties().value()) {
+        System.clearProperty(sysProp.name());
+      }
+    }
+
+    if (methodDescription.systemProperty() != null) {
+      System.clearProperty(methodDescription.systemProperty().name());
+    }
+    description = null;
+    methodDescription = null;
+    classDescription = null;
+    testRequiresSsh = false;
+
+    SystemReader.setInstance(oldSystemReader);
+    oldSystemReader = null;
+  }
+
+  public Description description() {
+    return description;
+  }
+
+  public GerritServer.Description methodDescription() {
+    return methodDescription;
+  }
+
+  public GerritServer.Description classDescription() {
+    return classDescription;
+  }
+
+  public boolean testRequiresSsh() {
+    return testRequiresSsh;
+  }
+
+  public Config baseConfig() {
+    return test.baseConfig;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java b/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java
new file mode 100644
index 0000000..631dc06
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.testing.TestTimeUtil;
+import java.sql.Timestamp;
+import java.time.Instant;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class TimeSettingsTestRule implements TestRule {
+  private final TestConfigRule config;
+  private String systemTimeZone;
+
+  public TimeSettingsTestRule(TestConfigRule config) {
+    this.config = config;
+  }
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        beforeTest();
+        statement.evaluate();
+        afterTest();
+      }
+    };
+  }
+
+  private void beforeTest() {
+    setTimeSettings(
+        config.classDescription().useSystemTime(),
+        config.classDescription().useClockStep(),
+        config.classDescription().useTimezone());
+    setTimeSettings(
+        config.methodDescription().useSystemTime(),
+        config.methodDescription().useClockStep(),
+        config.methodDescription().useTimezone());
+  }
+
+  private void afterTest() {
+    resetTimeSettings();
+  }
+
+  private void setTimeSettings(
+      boolean useSystemTime,
+      @Nullable UseClockStep useClockStep,
+      @Nullable UseTimezone useTimezone) {
+    if (useSystemTime) {
+      TestTimeUtil.useSystemTime();
+    } else if (useClockStep != null) {
+      TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
+      if (useClockStep.startAtEpoch()) {
+        TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+      }
+    }
+    if (useTimezone != null) {
+      systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
+    }
+  }
+
+  private void resetTimeSettings() {
+    TestTimeUtil.useSystemTime();
+    if (systemTimeZone != null) {
+      System.setProperty("user.timezone", systemTimeZone);
+      systemTimeZone = null;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/config/BUILD b/java/com/google/gerrit/acceptance/config/BUILD
index 0da68b0..1da975f 100644
--- a/java/com/google/gerrit/acceptance/config/BUILD
+++ b/java/com/google/gerrit/acceptance/config/BUILD
@@ -12,5 +12,6 @@
         "//lib:jgit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
     ],
 )
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/config/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/config/package-info.java
index 0709b86..753b75a 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/config/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.config;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/package-info.java
index 0709b86..428b5fb 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/rest/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/rest/package-info.java
index 0709b86..923b0c5 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/rest/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.rest;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
index 626092b..f20851c 100644
--- a/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
+++ b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
@@ -17,6 +17,10 @@
 import com.google.gerrit.sshd.CommandModule;
 
 public class TestSshCommandModule extends CommandModule {
+  public TestSshCommandModule() {
+    super(/* slaveMode= */ false);
+  }
+
   @Override
   protected void configure() {
     command("graceful").to(GracefulCommand.class);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/ssh/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/ssh/package-info.java
index 0709b86..940b42e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/ssh/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.ssh;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index edbb1ee..5b5895f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
@@ -66,10 +67,11 @@
 
   @Override
   public TestAccountCreation.Builder newAccount() {
-    return TestAccountCreation.builder(this::createAccount);
+    return TestAccountCreation.builder(
+        this::createAccount, externalIdFactory.arePasswordsAllowed());
   }
 
-  private Account.Id createAccount(TestAccountCreation testAccountCreation) throws Exception {
+  protected Account.Id createAccount(TestAccountCreation testAccountCreation) throws Exception {
     Account.Id accountId = Account.id(seq.nextAccountId());
     Consumer<AccountDelta.Builder> accountCreation =
         deltaBuilder -> initAccountDelta(deltaBuilder, testAccountCreation, accountId);
@@ -159,6 +161,7 @@
       checkState(updatedAccount.isPresent(), "Tried to update non-existing test account");
     }
 
+    @CanIgnoreReturnValue
     private Optional<AccountState> updateAccount(ConfigureDeltaFromState configureDeltaFromState)
         throws IOException, ConfigInvalidException {
       return accountsUpdate.update("Update Test Account", accountId, configureDeltaFromState);
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index 042dc9a..5d40517 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -18,6 +18,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.Account;
 import java.util.Optional;
@@ -41,50 +42,62 @@
 
   abstract ThrowingFunction<TestAccountCreation, Account.Id> accountCreator();
 
-  public static Builder builder(ThrowingFunction<TestAccountCreation, Account.Id> accountCreator) {
-    return new AutoValue_TestAccountCreation.Builder()
-        .accountCreator(accountCreator)
-        .httpPassword("http-pass");
+  public static Builder builder(
+      ThrowingFunction<TestAccountCreation, Account.Id> accountCreator,
+      boolean arePasswordsAllowed) {
+    TestAccountCreation.Builder builder =
+        new AutoValue_TestAccountCreation.Builder().accountCreator(accountCreator);
+    if (arePasswordsAllowed) {
+      builder.httpPassword("http-pass");
+    }
+    return builder;
   }
 
   @AutoValue.Builder
   public abstract static class Builder {
     public abstract Builder fullname(String fullname);
 
+    @CanIgnoreReturnValue
     public Builder clearFullname() {
       return fullname("");
     }
 
     public abstract Builder httpPassword(String httpPassword);
 
+    @CanIgnoreReturnValue
     public Builder clearHttpPassword() {
       return httpPassword("");
     }
 
     public abstract Builder preferredEmail(String preferredEmail);
 
+    @CanIgnoreReturnValue
     public Builder clearPreferredEmail() {
       return preferredEmail("");
     }
 
     public abstract Builder username(String username);
 
+    @CanIgnoreReturnValue
     public Builder clearUsername() {
       return username("");
     }
 
     public abstract Builder status(String status);
 
+    @CanIgnoreReturnValue
     public Builder clearStatus() {
       return status("");
     }
 
     abstract Builder active(boolean active);
 
+    @CanIgnoreReturnValue
     public Builder active() {
       return active(true);
     }
 
+    @CanIgnoreReturnValue
     public Builder inactive() {
       return active(false);
     }
@@ -93,6 +106,7 @@
 
     abstract ImmutableSet.Builder<String> secondaryEmailsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addSecondaryEmail(String secondaryEmail) {
       secondaryEmailsBuilder().add(secondaryEmail);
       return this;
@@ -103,6 +117,7 @@
 
     abstract TestAccountCreation autoBuild();
 
+    @CanIgnoreReturnValue
     public Account.Id create() {
       TestAccountCreation accountCreation = autoBuild();
       if (accountCreation.preferredEmail().isPresent()) {
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/account/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/account/package-info.java
index 0709b86..e211ecd 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.account;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index 3bd355b..1fd780f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -181,7 +181,8 @@
               side,
               message,
               unresolved,
-              parentUuid);
+              parentUuid,
+              null);
       // For draft comments, only the tag set on the HumanComment (and not on the ChangeUpdate)
       // matters.
       commentCreation.tag().ifPresent(tag -> newComment.tag = tag);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
index a0746e2..eb714d45 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -19,7 +19,9 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
@@ -32,6 +34,9 @@
 /** Initial attributes of the change. If not provided, arbitrary values will be used. */
 @AutoValue
 public abstract class TestChangeCreation {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public abstract Optional<String> host();
+
   public abstract Optional<Project.NameKey> project();
 
   public abstract String branch();
@@ -72,6 +77,10 @@
 
   @AutoValue.Builder
   public abstract static class Builder {
+    /** Host name in a multi-tenant deployment. */
+    @UsedAt(UsedAt.Project.GOOGLE)
+    public abstract Builder host(String host);
+
     /** Target project/Repository of the change. Must be an existing project. */
     public abstract Builder project(Project.NameKey project);
 
@@ -238,6 +247,7 @@
      *
      * @return the {@code Change.Id} of the created change
      */
+    @CanIgnoreReturnValue
     public Change.Id create() {
       TestChangeCreation changeUpdate = build();
       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
index 2031bde..9828e6c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.testsuite.change;
 
 import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
 import com.google.gerrit.common.Nullable;
@@ -68,6 +69,7 @@
   @AutoValue.Builder
   public abstract static class Builder {
 
+    @CanIgnoreReturnValue
     public Builder noMessage() {
       return message("");
     }
@@ -76,11 +78,13 @@
     public abstract Builder message(String message);
 
     /** Indicates a patchset-level comment. */
+    @CanIgnoreReturnValue
     public Builder onPatchsetLevel() {
       return file(Patch.PATCHSET_LEVEL);
     }
 
     /** Indicates a file comment. The comment will be on the specified file. */
+    @CanIgnoreReturnValue
     public Builder onFileLevelOf(String filePath) {
       return file(filePath).line(null).range(null);
     }
@@ -122,6 +126,7 @@
      * <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.
      */
+    @CanIgnoreReturnValue
     public Builder onPatchsetCommit() {
       return side(CommentSide.PATCHSET_COMMIT);
     }
@@ -135,11 +140,13 @@
      *
      * <p>For merge commits, this indicates the first parent commit.
      */
+    @CanIgnoreReturnValue
     public Builder onParentCommit() {
       return side(CommentSide.PARENT_COMMIT);
     }
 
     /** Like {@link #onParentCommit()} but for the second parent of a merge commit. */
+    @CanIgnoreReturnValue
     public Builder onSecondParentCommit() {
       return side(CommentSide.SECOND_PARENT_COMMIT);
     }
@@ -148,6 +155,7 @@
      * Like {@link #onParentCommit()} but for the AutoMerge commit created from the parents of a
      * merge commit.
      */
+    @CanIgnoreReturnValue
     public Builder onAutoMergeCommit() {
       return side(CommentSide.AUTO_MERGE_COMMIT);
     }
@@ -155,11 +163,13 @@
     abstract Builder side(CommentSide side);
 
     /** Indicates a resolved comment. */
+    @CanIgnoreReturnValue
     public Builder resolved() {
       return unresolved(false);
     }
 
     /** Indicates an unresolved comment. */
+    @CanIgnoreReturnValue
     public Builder unresolved() {
       return unresolved(true);
     }
@@ -211,6 +221,7 @@
      *
      * @return the UUID of the created comment
      */
+    @CanIgnoreReturnValue
     public String create() {
       TestCommentCreation commentCreation = autoBuild();
       return commentCreation.commentCreator().applyAndThrowSilently(commentCreation);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
index f8ca977..d9ac490 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
@@ -18,6 +18,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.PatchSet;
@@ -181,6 +182,7 @@
      *
      * @return the {@code PatchSet.Id} of the created patchset
      */
+    @CanIgnoreReturnValue
     public PatchSet.Id create() {
       TestPatchsetCreation patchsetCreation = build();
       return patchsetCreation.patchsetCreator().applyAndThrowSilently(patchsetCreation);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/change/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/change/package-info.java
index 0709b86..3863f72 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.change;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/BUILD b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
index 2052105..2bc2b62 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
@@ -21,6 +21,7 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:lang3",
+        "//lib/errorprone:annotations",
         "//lib/guice",
     ],
 )
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
index 99899cf..7549c84 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -54,6 +55,7 @@
 
     public abstract Builder description(String description);
 
+    @CanIgnoreReturnValue
     public Builder clearDescription() {
       return description("");
     }
@@ -62,10 +64,12 @@
 
     public abstract Builder visibleToAll(boolean visibleToAll);
 
+    @CanIgnoreReturnValue
     public Builder clearMembers() {
       return members(ImmutableSet.of());
     }
 
+    @CanIgnoreReturnValue
     public Builder members(Account.Id member1, Account.Id... otherMembers) {
       return members(Sets.union(ImmutableSet.of(member1), ImmutableSet.copyOf(otherMembers)));
     }
@@ -74,15 +78,18 @@
 
     abstract ImmutableSet.Builder<Account.Id> membersBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addMember(Account.Id member) {
       membersBuilder().add(member);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder clearSubgroups() {
       return subgroups(ImmutableSet.of());
     }
 
+    @CanIgnoreReturnValue
     public Builder subgroups(AccountGroup.UUID subgroup1, AccountGroup.UUID... otherSubgroups) {
       return subgroups(Sets.union(ImmutableSet.of(subgroup1), ImmutableSet.copyOf(otherSubgroups)));
     }
@@ -91,6 +98,7 @@
 
     abstract ImmutableSet.Builder<AccountGroup.UUID> subgroupsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addSubgroup(AccountGroup.UUID subgroup) {
       subgroupsBuilder().add(subgroup);
       return this;
@@ -106,6 +114,7 @@
      *
      * @return the UUID of the created group
      */
+    @CanIgnoreReturnValue
     public AccountGroup.UUID create() {
       TestGroupCreation groupCreation = autoBuild();
       return testRefAction(() -> groupCreation.groupCreator().applyAndThrowSilently(groupCreation));
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
index 47c7117..e375760 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -56,6 +57,7 @@
 
     public abstract Builder description(String description);
 
+    @CanIgnoreReturnValue
     public Builder clearDescription() {
       return description("");
     }
@@ -69,10 +71,12 @@
 
     abstract Function<ImmutableSet<Account.Id>, Set<Account.Id>> memberModification();
 
+    @CanIgnoreReturnValue
     public Builder clearMembers() {
       return memberModification(originalMembers -> ImmutableSet.of());
     }
 
+    @CanIgnoreReturnValue
     public Builder addMember(Account.Id member) {
       Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
           memberModification();
@@ -82,6 +86,7 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder removeMember(Account.Id member) {
       Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
           memberModification();
@@ -98,10 +103,12 @@
     abstract Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>>
         subgroupModification();
 
+    @CanIgnoreReturnValue
     public Builder clearSubgroups() {
       return subgroupModification(originalMembers -> ImmutableSet.of());
     }
 
+    @CanIgnoreReturnValue
     public Builder addSubgroup(AccountGroup.UUID subgroup) {
       Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
           subgroupModification();
@@ -111,6 +118,7 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder removeSubgroup(AccountGroup.UUID subgroup) {
       Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
           subgroupModification();
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/group/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/group/package-info.java
index 0709b86..78778e1 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.group;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/package-info.java
index 0709b86..2f43e09 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
index 4ac2705..be51a64 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -21,6 +21,7 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:lang3",
+        "//lib/errorprone:annotations",
         "//lib/guice",
     ],
 )
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 3337fc3..e8df7ed 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
@@ -66,6 +67,7 @@
      * "refs/heads/" prefix of the branch name can be omitted. The specified branches are ignored if
      * {@link #noEmptyCommit()} is used.
      */
+    @CanIgnoreReturnValue
     public TestProjectCreation.Builder branches(String branch1, String... otherBranches) {
       return branches(Sets.union(ImmutableSet.of(branch1), ImmutableSet.copyOf(otherBranches)));
     }
@@ -77,10 +79,12 @@
     public abstract TestProjectCreation.Builder permissionOnly(boolean value);
 
     /** Skips the empty commit on creation. This means that project's branches will not exist. */
+    @CanIgnoreReturnValue
     public TestProjectCreation.Builder noEmptyCommit() {
       return createEmptyCommit(false);
     }
 
+    @CanIgnoreReturnValue
     public TestProjectCreation.Builder addOwner(AccountGroup.UUID owner) {
       ownersBuilder().add(requireNonNull(owner, "owner"));
       return this;
@@ -98,6 +102,7 @@
      *
      * @return the name of the created project
      */
+    @CanIgnoreReturnValue
     public Project.NameKey create() {
       TestProjectCreation creation = autoBuild();
       return creation.projectCreator().applyAndThrowSilently(creation);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index 5634c78..cc57ba6 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -21,6 +21,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
@@ -75,6 +76,7 @@
       abstract Optional<Integer> max();
 
       /** Sets the minimum and maximum values for the capability. */
+      @CanIgnoreReturnValue
       public Builder range(int min, int max) {
         checkNonInvertedRange(min, max);
         return min(min).max(max);
@@ -353,61 +355,72 @@
      * Removes all access sections. Useful when testing against a specific set of access sections or
      * permissions.
      */
+    @CanIgnoreReturnValue
     public Builder removeAllAccessSections() {
       return removeAllAccessSections(true);
     }
 
     /** Adds a permission to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestPermission testPermission) {
       addedPermissionsBuilder().add(testPermission);
       return this;
     }
 
     /** Adds a permission to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestPermission.Builder testPermissionBuilder) {
       return add(testPermissionBuilder.build());
     }
 
     /** Adds a label permission to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestLabelPermission testLabelPermission) {
       addedLabelPermissionsBuilder().add(testLabelPermission);
       return this;
     }
 
     /** Adds a label permission to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestLabelPermission.Builder testLabelPermissionBuilder) {
       return add(testLabelPermissionBuilder.build());
     }
 
     /** Adds a capability to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestCapability testCapability) {
       addedCapabilitiesBuilder().add(testCapability);
       return this;
     }
 
     /** Adds a capability to be included in this update. */
+    @CanIgnoreReturnValue
     public Builder add(TestCapability.Builder testCapabilityBuilder) {
       return add(testCapabilityBuilder.build());
     }
 
     /** Removes a permission, label permission, or capability as part of this update. */
+    @CanIgnoreReturnValue
     public Builder remove(TestPermissionKey testPermissionKey) {
       removedPermissionsBuilder().add(testPermissionKey);
       return this;
     }
 
     /** Removes a permission, label permission, or capability as part of this update. */
+    @CanIgnoreReturnValue
     public Builder remove(TestPermissionKey.Builder testPermissionKeyBuilder) {
       return remove(testPermissionKeyBuilder.build());
     }
 
     /** Sets the exclusive bit bit for the given permission key. */
+    @CanIgnoreReturnValue
     public Builder setExclusiveGroup(
         TestPermissionKey.Builder testPermissionKeyBuilder, boolean exclusive) {
       return setExclusiveGroup(testPermissionKeyBuilder.build(), exclusive);
     }
 
     /** Sets the exclusive bit bit for the given permission key. */
+    @CanIgnoreReturnValue
     public Builder setExclusiveGroup(TestPermissionKey testPermissionKey, boolean exclusive) {
       checkArgument(
           !testPermissionKey.group().isPresent(),
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/project/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/project/package-info.java
index 0709b86..dfe56d6 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.project;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index a9914b3..8bad32c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.acceptance.testsuite.request;
 
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 
 /**
  * An aggregation of operations on Guice request scopes for test purposes.
@@ -25,54 +26,68 @@
  */
 public interface RequestScopeOperations {
   /**
-   * Sets the Guice request scope to the given account.
+   * Sets the Guice request scope to the given account without closing the existing context.
    *
-   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
-   * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
-   * must be enabled in the test and the account must have a username set. However, these are not
-   * requirements simply to call this method.
+   * <p>Returns newly created context. To restore previous context call {@link
+   * ManualRequestContext#close()} method of the returned context.
+   *
+   * <p>In order to create and use the SSH session for the newly set context, SSH must be enabled in
+   * the test and the account must have a username set.
+   *
+   * <p>The session associated with the returned context can be obtained by calling {@link
+   * com.google.gerrit.acceptance.AbstractDaemonTest#getOrCreateSshSessionForContext}.
    *
    * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
-   * @return the previous request scope.
    */
-  AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId);
+  ManualRequestContext setNestedApiUser(Account.Id accountId) throws Exception;
 
   /**
    * Sets the Guice request scope to the given account.
    *
-   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
-   * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
-   * must be enabled in the test and the account must have a username set. However, these are not
-   * requirements simply to call this method.
+   * <p>After calling this method, the new context can be obtained using the {@link
+   * ThreadLocalRequestContext#getContext()} method.
+   *
+   * <p>If the previous context is a {@link ManualRequestContext}. the method closes it before
+   * setting the new context. This prevents stacking of contexts.
+   *
+   * <p>In order to create and use the SSH session for the new context, SSH must be enabled in the
+   * test and the account must have a username set. To get the session associated with the newly set
+   * context use the {@link
+   * com.google.gerrit.acceptance.AbstractDaemonTest#getOrCreateSshSessionForContext} method.
+   *
+   * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
+   */
+  void setApiUser(Account.Id accountId) throws Exception;
+
+  /**
+   * Sets the Guice request scope to the given account.
+   *
+   * <p>See {@link #setApiUser(Account.Id)} for details.
    *
    * @param testAccount test account from {@code AccountOperations}.
-   * @return the previous request scope.
    */
-  AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount);
+  void setApiUser(TestAccount testAccount) throws Exception;
 
   /**
    * Enforces a new request context for the current API user.
    *
-   * <p>This recreates the {@code IdentifiedUser}, hence everything which is cached in the {@code
-   * IdentifiedUser} is reloaded (e.g. the email addresses of the user).
+   * <p>See {@link #setApiUser(Account.Id)} for details.
    *
-   * <p>The current user must be an identified user.
-   *
-   * @return the previous request scope.
+   * <p>The current user (i.e. a user set before calling this method) must be an identified user.
    */
-  AcceptanceTestRequestScope.Context resetCurrentApiUser();
+  void resetCurrentApiUser() throws Exception;
 
   /**
    * Sets the Guice request scope to the anonymous user.
    *
-   * @return the previous request scope.
+   * <p>See {@link #setApiUser(Account.Id)} for details.
    */
-  AcceptanceTestRequestScope.Context setApiUserAnonymous();
+  void setApiUserAnonymous() throws Exception;
 
   /**
    * Sets the Guice request scope to the internal server user.
    *
-   * @return the previous request scope.
+   * <p>See {@link #setApiUser(Account.Id)} for details.
    */
-  AcceptanceTestRequestScope.Context setApiUserInternal();
+  void setApiUserInternal() throws Exception;
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index 895c7a0..03336ca 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -17,12 +17,8 @@
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.Objects.requireNonNull;
 
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
-import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -30,10 +26,12 @@
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.net.InetSocketAddress;
 
 /**
  * The implementation of {@code RequestScopeOperations}.
@@ -43,64 +41,76 @@
  */
 @Singleton
 public class RequestScopeOperationsImpl implements RequestScopeOperations {
-  private final AcceptanceTestRequestScope atrScope;
+  private final ThreadLocalRequestContext localContext;
   private final AccountCache accountCache;
   private final AccountOperations accountOperations;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<AnonymousUser> anonymousUserProvider;
   private final InternalUser.Factory internalUserFactory;
-  private final InetSocketAddress sshAddress;
-  private final TestSshKeys testSshKeys;
 
   @Inject
   RequestScopeOperationsImpl(
-      AcceptanceTestRequestScope atrScope,
+      ThreadLocalRequestContext localContext,
       AccountCache accountCache,
       AccountOperations accountOperations,
       GenericFactory userFactory,
       Provider<AnonymousUser> anonymousUserProvider,
-      InternalUser.Factory internalUserFactory,
-      @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
-      TestSshKeys testSshKeys) {
-    this.atrScope = atrScope;
+      InternalUser.Factory internalUserFactory) {
+    this.localContext = localContext;
     this.accountCache = accountCache;
     this.accountOperations = accountOperations;
     this.userFactory = userFactory;
     this.anonymousUserProvider = anonymousUserProvider;
     this.internalUserFactory = internalUserFactory;
-    this.sshAddress = sshAddress;
-    this.testSshKeys = testSshKeys;
   }
 
   @Override
-  public AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId) {
-    return setApiUser(accountOperations.account(accountId).get());
+  public ManualRequestContext setNestedApiUser(Account.Id accountId) {
+    return new ManualRequestContext(createIdentifiedUser(accountId), localContext);
   }
 
   @Override
-  public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
-    return atrScope.set(
-        atrScope.newContext(
-            SshSessionFactory.createSession(testSshKeys, sshAddress, testAccount),
-            createIdentifiedUser(testAccount.accountId())));
+  public void setApiUser(Account.Id accountId) {
+    setApiUser(accountOperations.account(accountId).get());
   }
 
   @Override
-  public AcceptanceTestRequestScope.Context resetCurrentApiUser() {
-    CurrentUser user = atrScope.get().getUser();
+  public void setApiUser(TestAccount testAccount) {
+    setApiUser(createIdentifiedUser(testAccount.accountId()));
+  }
+
+  @Override
+  public void resetCurrentApiUser() {
+    RequestContext currentContext = localContext.getContext();
+    checkState(
+        currentContext != null, "can only reset IdentifiedUser, but the RequestContext is null");
+    CurrentUser user = localContext.getContext().getUser();
     // More special cases for anonymous users etc. can be added as needed.
     checkState(user.isIdentifiedUser(), "can only reset IdentifiedUser, not %s", user);
-    return setApiUser(user.getAccountId());
+    setApiUser(user.getAccountId());
   }
 
   @Override
-  public AcceptanceTestRequestScope.Context setApiUserAnonymous() {
-    return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
+  public void setApiUserAnonymous() {
+    setApiUser(anonymousUserProvider.get());
   }
 
   @Override
-  public AcceptanceTestRequestScope.Context setApiUserInternal() {
-    return atrScope.set(atrScope.newContext(null, internalUserFactory.create()));
+  public void setApiUserInternal() {
+    setApiUser(internalUserFactory.create());
+  }
+
+  private void setApiUser(CurrentUser newUser) {
+    // The ManualRequestContext stores the previous context. When the close() method is called,
+    // the old context is restored.
+    RequestContext oldContext = localContext.getContext();
+    if (oldContext instanceof ManualRequestContext) {
+      ((ManualRequestContext) oldContext).close();
+    }
+    // The created object is not used, because the constructor of the ManualRequestContext sets the
+    // active context to itself. It is not needed to explicitly call localContext.setContext after
+    // an instance is created.
+    var unused = new ManualRequestContext(newUser, localContext);
   }
 
   private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/acceptance/testsuite/request/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/acceptance/testsuite/request/package-info.java
index 0709b86..943bd21 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.request;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
index 9d0a28e..25ed813 100644
--- a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
+++ b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
@@ -22,7 +22,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -164,7 +164,7 @@
     if (bazel) {
       renderFiles(inputFiles, null);
     } else {
-      try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFile)))) {
+      try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Path.of(zipFile)))) {
         renderFiles(inputFiles, zip);
 
         File[] cssFiles = tmpdir.listFiles((dir, name) -> name.endsWith(".css"));
diff --git a/java/com/google/gerrit/asciidoctor/BUILD b/java/com/google/gerrit/asciidoctor/BUILD
index 94ec20d..a132095 100644
--- a/java/com/google/gerrit/asciidoctor/BUILD
+++ b/java/com/google/gerrit/asciidoctor/BUILD
@@ -35,6 +35,6 @@
         "//lib:args4j",
         "//lib:guava",
         "//lib/lucene:lucene-analyzers-common",
-        "//lib/lucene:lucene-core-and-backward-codecs",
+        "//lib/lucene:lucene-core",
     ],
 )
diff --git a/java/com/google/gerrit/asciidoctor/DocIndexer.java b/java/com/google/gerrit/asciidoctor/DocIndexer.java
index acd6aad..fd8161b 100644
--- a/java/com/google/gerrit/asciidoctor/DocIndexer.java
+++ b/java/com/google/gerrit/asciidoctor/DocIndexer.java
@@ -24,7 +24,7 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.jar.JarEntry;
@@ -82,7 +82,7 @@
       return;
     }
 
-    try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Paths.get(outFile)))) {
+    try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Path.of(outFile)))) {
       byte[] compressedIndex = zip(index());
       JarEntry entry = new JarEntry(String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
       entry.setSize(compressedIndex.length);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/asciidoctor/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/asciidoctor/package-info.java
index 0709b86..504c3f5 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/asciidoctor/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.asciidoctor;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 7dc2b1b..e33e5cb 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -331,7 +331,10 @@
     final DirContext ctx = helper.open();
     try {
       Helper.LdapSchema schema = helper.getSchema(ctx);
-      helper.findAccount(schema, ctx, username, false);
+
+      @SuppressWarnings("unused")
+      var unused = helper.findAccount(schema, ctx, username, false);
+
       return true;
     } catch (NoSuchUserException e) {
       return false;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/auth/ldap/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/auth/ldap/package-info.java
index 0709b86..45e1bc2 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/auth/ldap/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.auth.ldap;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/auth/oauth/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/auth/oauth/package-info.java
index 0709b86..53c6829 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/auth/oauth/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.auth.oauth;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/auth/openid/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/auth/openid/package-info.java
index 0709b86..2b6f77d 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/auth/openid/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.auth.openid;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/auth/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/auth/package-info.java
index 0709b86..6538de4 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/auth/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.auth;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 8f930bb..8f85311 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -30,6 +30,7 @@
         "//lib:servlet-api",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//lib/flogger:api",
     ],
 )
diff --git a/java/com/google/gerrit/common/FileUtil.java b/java/com/google/gerrit/common/FileUtil.java
index 5b0925e..35cf848 100644
--- a/java/com/google/gerrit/common/FileUtil.java
+++ b/java/com/google/gerrit/common/FileUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -84,6 +85,7 @@
     }
   }
 
+  @CanIgnoreReturnValue
   public static Path mkdirsOrDie(Path p, String errMsg) {
     try {
       if (!Files.isDirectory(p)) {
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 5ea5177..83af551 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -41,6 +41,7 @@
     PLUGIN_CHECKS,
     PLUGIN_CODE_OWNERS,
     PLUGIN_DELETE_PROJECT,
+    PLUGIN_GITHUB,
     PLUGIN_HIGH_AVAILABILITY,
     PLUGIN_MULTI_SITE,
     PLUGIN_PULL_REPLICATION,
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/common/auth/openid/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/common/auth/openid/package-info.java
index 0709b86..931ed56 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/common/auth/openid/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.common.auth.openid;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/common/data/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/common/data/package-info.java
index 0709b86..9f3539c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/common/data/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.common.data;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/data/testing/BUILD b/java/com/google/gerrit/common/data/testing/BUILD
index d39d05c..a580c77 100644
--- a/java/com/google/gerrit/common/data/testing/BUILD
+++ b/java/com/google/gerrit/common/data/testing/BUILD
@@ -7,6 +7,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/entities",
+        "//lib/errorprone:annotations",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/common/data/testing/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/common/data/testing/package-info.java
index 0709b86..24c3708 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/common/data/testing/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.common.data.testing;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/common/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/common/package-info.java
index 0709b86..5f97d56 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/common/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.common;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/entities/AccessSection.java b/java/com/google/gerrit/entities/AccessSection.java
index 8ae0a5d..7dca72e 100644
--- a/java/com/google/gerrit/entities/AccessSection.java
+++ b/java/com/google/gerrit/entities/AccessSection.java
@@ -20,6 +20,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import java.util.ArrayList;
 import java.util.List;
@@ -121,27 +122,32 @@
 
     public abstract String getName();
 
+    @CanIgnoreReturnValue
     public Builder modifyPermissions(Consumer<List<Permission.Builder>> modification) {
       modification.accept(permissionBuilders);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addPermission(Permission.Builder permission) {
       requireNonNull(permission, "permission must be non-null");
       return modifyPermissions(p -> p.add(permission));
     }
 
+    @CanIgnoreReturnValue
     public Builder remove(Permission.Builder permission) {
       requireNonNull(permission, "permission must be non-null");
       return removePermission(permission.getName());
     }
 
+    @CanIgnoreReturnValue
     public Builder removePermission(String name) {
       requireNonNull(name, "name must be non-null");
       return modifyPermissions(
           p -> p.removeIf(permissionBuilder -> name.equalsIgnoreCase(permissionBuilder.getName())));
     }
 
+    @CanIgnoreReturnValue
     public Permission.Builder upsertPermission(String permissionName) {
       requireNonNull(permissionName, "permissionName must be non-null");
 
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 52ad0a9..efbac97 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -19,7 +19,9 @@
 import static com.google.gerrit.entities.RefNames.REFS_USERS;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
 import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import java.time.Instant;
@@ -160,6 +162,17 @@
   public abstract String metaId();
 
   /**
+   * A unique tag which identifies the current version of the account.
+   *
+   * <p>It can be any non-empty string. For open-source gerrit it is the same as metaId; internally
+   * in google a different value is assigned.
+   *
+   * <p>The value can be null only during account updating/creation.
+   */
+  @Nullable
+  public abstract String uniqueTag();
+
+  /**
    * Create a new account.
    *
    * @param newId unique id, see Sequences#nextAccountId().
@@ -261,6 +274,7 @@
 
     public abstract Builder setInactive(boolean inactive);
 
+    @CanIgnoreReturnValue
     public Builder setActive(boolean active) {
       return setInactive(!active);
     }
@@ -275,6 +289,11 @@
 
     public abstract Builder setMetaId(@Nullable String metaId);
 
+    @Nullable
+    public abstract String uniqueTag();
+
+    public abstract Builder setUniqueTag(@Nullable String uniqueTag);
+
     public abstract Account build();
   }
 
@@ -282,4 +301,18 @@
   public final String toString() {
     return getName();
   }
+
+  public final String debugString() {
+    return MoreObjects.toStringHelper(this)
+        .add("id", id())
+        .add("registeredOn", registeredOn())
+        .add("fullName", fullName())
+        .add("displayName", displayName())
+        .add("preferredEmail", preferredEmail())
+        .add("inactive", inactive())
+        .add("status", status())
+        .add("metaId", metaId())
+        .add("uniqueTag", uniqueTag())
+        .toString();
+  }
 }
diff --git a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
index 0ef51e5..5db5af5 100644
--- a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.time.Instant;
 import java.util.Optional;
 
@@ -39,6 +40,7 @@
 
     abstract Builder removedOn(Instant removedOn);
 
+    @CanIgnoreReturnValue
     public Builder removed(Account.Id removedBy, Instant removedOn) {
       return removedBy(removedBy).removedOn(removedOn);
     }
diff --git a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
index 913956e..0cb1c62 100644
--- a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.time.Instant;
 import java.util.Optional;
 
@@ -43,10 +44,12 @@
 
     abstract Builder removedOn(Instant removedOn);
 
+    @CanIgnoreReturnValue
     public Builder removed(Account.Id removedBy, Instant removedOn) {
       return removedBy(removedBy).removedOn(removedOn);
     }
 
+    @CanIgnoreReturnValue
     public Builder removedLegacy() {
       return removed(addedBy(), addedOn());
     }
diff --git a/java/com/google/gerrit/entities/CachedProjectConfig.java b/java/com/google/gerrit/entities/CachedProjectConfig.java
index be4a1cf..6dc9e32 100644
--- a/java/com/google/gerrit/entities/CachedProjectConfig.java
+++ b/java/com/google/gerrit/entities/CachedProjectConfig.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -148,31 +149,37 @@
 
     public abstract Builder setBranchOrderSection(Optional<BranchOrderSection> value);
 
+    @CanIgnoreReturnValue
     public Builder addGroup(GroupReference groupReference) {
       groupsBuilder().put(groupReference.getUUID(), groupReference);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addAccessSection(AccessSection accessSection) {
       accessSectionsBuilder().put(accessSection.getName(), accessSection);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addContributorAgreement(ContributorAgreement contributorAgreement) {
       contributorAgreementsBuilder().put(contributorAgreement.getName(), contributorAgreement);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addNotifySection(NotifyConfig notifyConfig) {
       notifySectionsBuilder().put(notifyConfig.getName(), notifyConfig);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addLabelSection(LabelType labelType) {
       labelSectionsBuilder().put(labelType.getName(), labelType);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addSubmitRequirementSection(SubmitRequirement submitRequirement) {
       submitRequirementSectionsBuilder().put(submitRequirement.name(), submitRequirement);
       return this;
@@ -180,11 +187,13 @@
 
     public abstract Builder setMimeTypes(ConfiguredMimeTypes value);
 
+    @CanIgnoreReturnValue
     public Builder addSubscribeSection(SubscribeSection subscribeSection) {
       subscribeSectionsBuilder().put(subscribeSection.project(), subscribeSection);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addCommentLinkSection(StoredCommentLinkInfo storedCommentLinkInfo) {
       commentLinkSectionsBuilder().put(storedCommentLinkInfo.getName(), storedCommentLinkInfo);
       return this;
@@ -213,6 +222,7 @@
 
     abstract ImmutableMap.Builder<String, String> pluginConfigsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addPluginConfig(String pluginName, String pluginConfig) {
       pluginConfigsBuilder().put(pluginName, pluginConfig);
       return this;
@@ -222,6 +232,7 @@
 
     abstract ImmutableMap.Builder<String, ImmutableConfig> parsedProjectLevelConfigsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addProjectLevelConfig(String configFileName, String config) {
       projectLevelConfigsBuilder().put(configFileName, config);
       try {
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index e1e143c..35a60eb 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -20,6 +20,7 @@
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -216,7 +217,7 @@
   public int lineNbr;
 
   public Identity author;
-  protected Identity realAuthor;
+  public Identity realAuthor;
 
   // TODO(issue-15525): Migrate this field from Timestamp to Instant
   public Timestamp writtenOn;
@@ -227,6 +228,8 @@
   public Range range;
   public String tag;
 
+  @Nullable public List<FixSuggestion> fixSuggestions;
+
   /**
    * Hex commit SHA1 of the commit of the patchset to which this comment applies. Other classes call
    * this "commitId", but this class uses the old ReviewDb term "revId", and this field name is
@@ -300,7 +303,14 @@
         + (key != null ? nullableLength(key.filename, key.uuid) : 0);
   }
 
-  public abstract int getApproximateSize();
+  public int getApproximateSize() {
+    int approximateSize = getCommentFieldApproximateSize();
+    approximateSize +=
+        fixSuggestions != null
+            ? fixSuggestions.stream().mapToInt(FixSuggestion::getApproximateSize).sum()
+            : 0;
+    return approximateSize;
+  }
 
   static int nullableLength(String... strings) {
     int length = 0;
@@ -327,7 +337,8 @@
         && Objects.equals(range, c.range)
         && Objects.equals(tag, c.tag)
         && Objects.equals(revId, c.revId)
-        && Objects.equals(serverId, c.serverId);
+        && Objects.equals(serverId, c.serverId)
+        && Objects.equals(fixSuggestions, c.fixSuggestions);
   }
 
   @Override
@@ -344,7 +355,8 @@
         range,
         tag,
         revId,
-        serverId);
+        serverId,
+        fixSuggestions);
   }
 
   @Override
@@ -364,6 +376,7 @@
         .add("parentUuid", Objects.toString(parentUuid, ""))
         .add("range", Objects.toString(range, ""))
         .add("revId", Objects.toString(revId, ""))
-        .add("tag", Objects.toString(tag, ""));
+        .add("tag", Objects.toString(tag, ""))
+        .add("fixSuggestions", Objects.toString(fixSuggestions, ""));
   }
 }
diff --git a/java/com/google/gerrit/entities/FixReplacement.java b/java/com/google/gerrit/entities/FixReplacement.java
index fbbf746..aa15ffc 100644
--- a/java/com/google/gerrit/entities/FixReplacement.java
+++ b/java/com/google/gerrit/entities/FixReplacement.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.entities;
 
+import java.util.Objects;
+
 public final class FixReplacement {
   public final String path;
   public final Comment.Range range;
@@ -43,4 +45,20 @@
   int getApproximateSize() {
     return path.length() + replacement.length();
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof FixReplacement)) {
+      return false;
+    }
+    FixReplacement f = (FixReplacement) o;
+    return Objects.equals(path, f.path)
+        && Objects.equals(range, f.range)
+        && Objects.equals(replacement, f.replacement);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(path, range, replacement);
+  }
 }
diff --git a/java/com/google/gerrit/entities/FixSuggestion.java b/java/com/google/gerrit/entities/FixSuggestion.java
index 892e324..737c23e 100644
--- a/java/com/google/gerrit/entities/FixSuggestion.java
+++ b/java/com/google/gerrit/entities/FixSuggestion.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import java.util.List;
+import java.util.Objects;
 
 public final class FixSuggestion {
   public final String fixId;
@@ -47,4 +48,20 @@
         + description.length()
         + replacements.stream().mapToInt(FixReplacement::getApproximateSize).sum();
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof FixSuggestion)) {
+      return false;
+    }
+    FixSuggestion fs = (FixSuggestion) o;
+    return Objects.equals(fixId, fs.fixId)
+        && Objects.equals(description, fs.description)
+        && Objects.equals(replacements, fs.replacements);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(fixId, description, replacements);
+  }
 }
diff --git a/java/com/google/gerrit/entities/HumanComment.java b/java/com/google/gerrit/entities/HumanComment.java
index d287fa0..1e48f11 100644
--- a/java/com/google/gerrit/entities/HumanComment.java
+++ b/java/com/google/gerrit/entities/HumanComment.java
@@ -47,11 +47,6 @@
   }
 
   @Override
-  public int getApproximateSize() {
-    return super.getCommentFieldApproximateSize();
-  }
-
-  @Override
   public String toString() {
     return toStringHelper().add("unresolved", unresolved).toString();
   }
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index 7a3266ecc..ff4b8f9 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -20,6 +20,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import java.util.ArrayList;
 import java.util.List;
@@ -39,6 +40,7 @@
     return create(name, values);
   }
 
+  @CanIgnoreReturnValue
   public static String checkName(String name) throws IllegalArgumentException {
     checkNameInternal(name);
     if ("SUBM".equals(name)) {
@@ -47,6 +49,7 @@
     return name;
   }
 
+  @CanIgnoreReturnValue
   public static String checkNameInternal(String name) throws IllegalArgumentException {
     if (name == null || name.isEmpty()) {
       throw new IllegalArgumentException("Empty label name");
@@ -246,7 +249,7 @@
         setRefPatterns(null);
       }
 
-      List<LabelValue> valueList = sortValues(getValues());
+      ImmutableList<LabelValue> valueList = sortValues(getValues());
       setValues(valueList);
       if (!valueList.isEmpty()) {
         if (valueList.get(0).getValue() < 0) {
diff --git a/java/com/google/gerrit/entities/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
index 5c0a3db..d3123c4 100644
--- a/java/com/google/gerrit/entities/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -18,6 +18,7 @@
 import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import java.util.EnumSet;
 import java.util.Set;
@@ -74,11 +75,13 @@
 
     public abstract Builder setHeader(Header hdr);
 
+    @CanIgnoreReturnValue
     public Builder addGroup(GroupReference group) {
       groupsBuilder().add(group);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addAddress(Address address) {
       addressesBuilder().add(address);
       return this;
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 0e959e7..1f2f151 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -18,6 +18,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -274,15 +275,18 @@
 
     public abstract Builder setExclusiveGroup(boolean value);
 
+    @CanIgnoreReturnValue
     public Builder modifyRules(Consumer<List<PermissionRule.Builder>> modification) {
       modification.accept(rulesBuilders);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder add(PermissionRule.Builder rule) {
       return modifyRules(r -> r.add(rule));
     }
 
+    @CanIgnoreReturnValue
     public Builder remove(PermissionRule rule) {
       if (rule != null) {
         return removeRule(rule.getGroup());
@@ -290,10 +294,12 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder removeRule(GroupReference group) {
       return modifyRules(rules -> rules.removeIf(rule -> sameGroup(rule.build(), group)));
     }
 
+    @CanIgnoreReturnValue
     public Builder clearRules() {
       return modifyRules(r -> r.clear());
     }
diff --git a/java/com/google/gerrit/entities/PermissionRule.java b/java/com/google/gerrit/entities/PermissionRule.java
index 1665c1c..706091f 100644
--- a/java/com/google/gerrit/entities/PermissionRule.java
+++ b/java/com/google/gerrit/entities/PermissionRule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
 @AutoValue
 public abstract class PermissionRule implements Comparable<PermissionRule> {
@@ -239,14 +240,17 @@
 
   @AutoValue.Builder
   public abstract static class Builder {
+    @CanIgnoreReturnValue
     public Builder setDeny() {
       return setAction(Action.DENY);
     }
 
+    @CanIgnoreReturnValue
     public Builder setBlock() {
       return setAction(Action.BLOCK);
     }
 
+    @CanIgnoreReturnValue
     public Builder setRange(int newMin, int newMax) {
       if (newMax < newMin) {
         setMin(newMax);
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 72ca6a9..9c2866c 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -18,6 +18,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -195,6 +196,7 @@
   public abstract static class Builder {
     public abstract Builder setDescription(String description);
 
+    @CanIgnoreReturnValue
     public Builder setBooleanConfig(BooleanProjectConfig config, InheritableBoolean val) {
       Map<BooleanProjectConfig, InheritableBoolean> map = new HashMap<>(getBooleanConfigs());
       map.replace(config, val);
@@ -214,6 +216,7 @@
 
     public abstract Builder setParent(NameKey n);
 
+    @CanIgnoreReturnValue
     public Builder setParent(String n) {
       return setParent(n != null ? nameKey(n) : null);
     }
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
index 1d46d3b..a4288ca 100644
--- a/java/com/google/gerrit/entities/RobotComment.java
+++ b/java/com/google/gerrit/entities/RobotComment.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.entities;
 
 import java.time.Instant;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+@Deprecated
 public final class RobotComment extends Comment {
   public String robotId;
   public String robotRunId;
   public String url;
   public Map<String, String> properties;
-  public List<FixSuggestion> fixSuggestions;
 
   public RobotComment(
       Key key,
@@ -64,7 +63,6 @@
         .add("robotRunId", robotRunId)
         .add("url", url)
         .add("properties", Objects.toString(properties, ""))
-        .add("fixSuggestions", Objects.toString(fixSuggestions, ""))
         .toString();
   }
 
@@ -78,12 +76,11 @@
         && Objects.equals(robotId, c.robotId)
         && Objects.equals(robotRunId, c.robotRunId)
         && Objects.equals(url, c.url)
-        && Objects.equals(properties, c.properties)
-        && Objects.equals(fixSuggestions, c.fixSuggestions);
+        && Objects.equals(properties, c.properties);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(super.hashCode(), robotId, robotRunId, url, properties, fixSuggestions);
+    return Objects.hash(super.hashCode(), robotId, robotRunId, url, properties);
   }
 }
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index fbb2fd7..f9a5aeb 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -16,6 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gson.Gson;
 import com.google.gson.TypeAdapter;
 import java.util.Optional;
@@ -203,6 +204,7 @@
 
       public abstract Builder status(boolean value);
 
+      @CanIgnoreReturnValue
       public Builder addChildPredicateResult(PredicateResult result) {
         childPredicateResultsBuilder().add(result);
         return this;
diff --git a/java/com/google/gerrit/entities/SubscribeSection.java b/java/com/google/gerrit/entities/SubscribeSection.java
index 574cae8..7751907 100644
--- a/java/com/google/gerrit/entities/SubscribeSection.java
+++ b/java/com/google/gerrit/entities/SubscribeSection.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
@@ -51,12 +52,14 @@
 
     abstract ImmutableList.Builder<RefSpec> multiMatchRefSpecsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addMatchingRefSpec(String matchingRefSpec) {
       matchingRefSpecsBuilder()
           .add(new RefSpec(matchingRefSpec, RefSpec.WildcardMode.REQUIRE_MATCH));
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addMultiMatchRefSpec(String multiMatchRefSpec) {
       multiMatchRefSpecsBuilder()
           .add(new RefSpec(multiMatchRefSpec, RefSpec.WildcardMode.ALLOW_MISMATCH));
diff --git a/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
new file mode 100644
index 0000000..c073a5f
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link AccountInput} and {@link
+ * com.google.gerrit.proto.Entities.AccountInput}.
+ */
+@Immutable
+public enum AccountInputProtoConverter
+    implements ProtoConverter<Entities.AccountInput, AccountInput> {
+  INSTANCE;
+
+  @Override
+  public Entities.AccountInput toProto(AccountInput accountInput) {
+    Entities.AccountInput.Builder builder = Entities.AccountInput.newBuilder();
+    if (accountInput.username != null) {
+      builder.setUsername(accountInput.username);
+    }
+    if (accountInput.name != null) {
+      builder.setName(accountInput.name);
+    }
+    if (accountInput.displayName != null) {
+      builder.setDisplayName(accountInput.displayName);
+    }
+    if (accountInput.email != null) {
+      builder.setEmail(accountInput.email);
+    }
+    if (accountInput.sshKey != null) {
+      builder.setSshKey(accountInput.sshKey);
+    }
+    if (accountInput.httpPassword != null) {
+      builder.setHttpPassword(accountInput.httpPassword);
+    }
+    if (accountInput.groups != null) {
+      builder.addAllGroups(accountInput.groups);
+    }
+
+    return builder.build();
+  }
+
+  @Override
+  public AccountInput fromProto(Entities.AccountInput proto) {
+    AccountInput accountInput = new AccountInput();
+    if (proto.hasUsername()) {
+      accountInput.username = proto.getUsername();
+    }
+    if (proto.hasName()) {
+      accountInput.name = proto.getName();
+    }
+    if (proto.hasDisplayName()) {
+      accountInput.displayName = proto.getDisplayName();
+    }
+    if (proto.hasEmail()) {
+      accountInput.email = proto.getEmail();
+    }
+    if (proto.hasSshKey()) {
+      accountInput.sshKey = proto.getSshKey();
+    }
+    if (proto.hasHttpPassword()) {
+      accountInput.httpPassword = proto.getHttpPassword();
+    }
+    if (proto.getGroupsCount() > 0) {
+      accountInput.groups = proto.getGroupsList();
+    }
+    return accountInput;
+  }
+
+  @Override
+  public Parser<Entities.AccountInput> getParser() {
+    return Entities.AccountInput.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java b/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java
new file mode 100644
index 0000000..fc862b0
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link ApplyPatchInput} and {@link
+ * com.google.gerrit.proto.Entities.ApplyPatchInput}.
+ */
+@Immutable
+public enum ApplyPatchInputProtoConverter
+    implements ProtoConverter<Entities.ApplyPatchInput, ApplyPatchInput> {
+  INSTANCE;
+
+  @Override
+  public Entities.ApplyPatchInput toProto(ApplyPatchInput applyPatchInput) {
+    Entities.ApplyPatchInput.Builder builder = Entities.ApplyPatchInput.newBuilder();
+    if (applyPatchInput.patch != null) {
+      builder.setPatch(applyPatchInput.patch);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public ApplyPatchInput fromProto(Entities.ApplyPatchInput proto) {
+    ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+    if (proto.hasPatch()) {
+      applyPatchInput.patch = proto.getPatch();
+    }
+    return applyPatchInput;
+  }
+
+  @Override
+  public Parser<Entities.ApplyPatchInput> getParser() {
+    return Entities.ApplyPatchInput.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
new file mode 100644
index 0000000..81c7878
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Proto converter between {@link ChangeInput} and {@link
+ * com.google.gerrit.proto.Entities.ChangeInput}.
+ */
+@Immutable
+public enum ChangeInputProtoConverter implements ProtoConverter<Entities.ChangeInput, ChangeInput> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.MergeInput, MergeInput> mergeInputConverter =
+      MergeInputProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.ApplyPatchInput, ApplyPatchInput> applyPatchInputConverter =
+      ApplyPatchInputProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.AccountInput, AccountInput> accountInputConverter =
+      AccountInputProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.NotifyInfo, NotifyInfo> notifyInfoConverter =
+      NotifyInfoProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.ChangeInput toProto(ChangeInput changeInput) {
+    Entities.ChangeInput.Builder builder = Entities.ChangeInput.newBuilder();
+    if (changeInput.project != null) {
+      builder.setProject(changeInput.project);
+    }
+    if (changeInput.branch != null) {
+      builder.setBranch(changeInput.branch);
+    }
+    if (changeInput.subject != null) {
+      builder.setSubject(changeInput.subject);
+    }
+    if (changeInput.topic != null) {
+      builder.setTopic(changeInput.topic);
+    }
+    if (changeInput.status != null) {
+      builder.setStatus(Entities.ChangeStatus.forNumber(changeInput.status.getValue()));
+    }
+    if (changeInput.isPrivate != null) {
+      builder.setIsPrivate(changeInput.isPrivate);
+    }
+    if (changeInput.workInProgress != null) {
+      builder.setWorkInProgress(changeInput.workInProgress);
+    }
+    if (changeInput.baseChange != null) {
+      builder.setBaseChange(changeInput.baseChange);
+    }
+    if (changeInput.baseCommit != null) {
+      builder.setBaseCommit(changeInput.baseCommit);
+    }
+    if (changeInput.newBranch != null) {
+      builder.setNewBranch(changeInput.newBranch);
+    }
+    if (changeInput.validationOptions != null) {
+      builder.putAllValidationOptions(changeInput.validationOptions);
+    }
+    if (changeInput.customKeyedValues != null) {
+      builder.putAllCustomKeyedValues(changeInput.customKeyedValues);
+    }
+    if (changeInput.merge != null) {
+      builder.setMerge(mergeInputConverter.toProto(changeInput.merge));
+    }
+    if (changeInput.patch != null) {
+      builder.setPatch(applyPatchInputConverter.toProto(changeInput.patch));
+    }
+    if (changeInput.author != null) {
+      builder.setAuthor(accountInputConverter.toProto(changeInput.author));
+    }
+    builder.setNotify(Entities.NotifyHandling.forNumber(changeInput.notify.getValue()));
+
+    List<ListChangesOption> responseFormatOptions = changeInput.responseFormatOptions;
+    if (responseFormatOptions != null) {
+      for (ListChangesOption option : responseFormatOptions) {
+        builder.addResponseFormatOptions(Entities.ListChangesOption.forNumber(option.getValue()));
+      }
+    }
+
+    if (changeInput.notifyDetails != null) {
+      Map<RecipientType, NotifyInfo> notifyDetails = changeInput.notifyDetails;
+      for (Map.Entry<RecipientType, NotifyInfo> entry : notifyDetails.entrySet()) {
+        Entities.RecipientType recipientType =
+            Entities.RecipientType.forNumber(entry.getKey().getValue());
+        builder.putNotifyDetails(
+            recipientType.name(), notifyInfoConverter.toProto(entry.getValue()));
+      }
+    }
+    return builder.build();
+  }
+
+  @Override
+  public ChangeInput fromProto(Entities.ChangeInput proto) {
+    ChangeInput changeInput =
+        new ChangeInput(proto.getProject(), proto.getBranch(), proto.getSubject());
+    if (proto.hasTopic()) {
+      changeInput.topic = proto.getTopic();
+    }
+    if (proto.hasStatus()) {
+      changeInput.status = ChangeStatus.valueOf(proto.getStatus().name());
+    }
+    if (proto.hasIsPrivate()) {
+      changeInput.isPrivate = proto.getIsPrivate();
+    }
+    if (proto.hasWorkInProgress()) {
+      changeInput.workInProgress = proto.getWorkInProgress();
+    }
+    if (proto.hasBaseChange()) {
+      changeInput.baseChange = proto.getBaseChange();
+    }
+    if (proto.hasBaseCommit()) {
+      changeInput.baseCommit = proto.getBaseCommit();
+    }
+    if (proto.hasNewBranch()) {
+      changeInput.newBranch = proto.getNewBranch();
+    }
+    if (proto.getValidationOptionsCount() > 0) {
+      changeInput.validationOptions = proto.getValidationOptionsMap();
+    }
+    if (proto.getCustomKeyedValuesCount() > 0) {
+      changeInput.customKeyedValues = proto.getCustomKeyedValuesMap();
+    }
+    if (proto.hasMerge()) {
+      changeInput.merge = mergeInputConverter.fromProto(proto.getMerge());
+    }
+    if (proto.hasPatch()) {
+      changeInput.patch = applyPatchInputConverter.fromProto(proto.getPatch());
+    }
+    if (proto.hasAuthor()) {
+      changeInput.author = accountInputConverter.fromProto(proto.getAuthor());
+    }
+    if (proto.getResponseFormatOptionsCount() > 0) {
+      changeInput.responseFormatOptions = new ArrayList<ListChangesOption>();
+      for (Entities.ListChangesOption option : proto.getResponseFormatOptionsList()) {
+        changeInput.responseFormatOptions.add(ListChangesOption.valueOf(option.name()));
+      }
+    }
+
+    changeInput.notify = NotifyHandling.valueOf(proto.getNotify().name());
+
+    if (proto.getNotifyDetailsCount() > 0) {
+      changeInput.notifyDetails = new HashMap<RecipientType, NotifyInfo>();
+      for (Map.Entry<String, Entities.NotifyInfo> entry : proto.getNotifyDetailsMap().entrySet()) {
+        changeInput.notifyDetails.put(
+            RecipientType.valueOf(entry.getKey()), notifyInfoConverter.fromProto(entry.getValue()));
+      }
+    }
+
+    return changeInput;
+  }
+
+  @Override
+  public Parser<Entities.ChangeInput> getParser() {
+    return Entities.ChangeInput.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 3b772d0..93cacaf 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -79,6 +79,10 @@
     if (cherryPickOf != null) {
       builder.setCherryPickOf(patchSetIdConverter.toProto(cherryPickOf));
     }
+    String serverId = change.getServerId();
+    if (serverId != null) {
+      builder.setServerId(serverId);
+    }
     return builder.build();
   }
 
@@ -119,6 +123,9 @@
     if (proto.hasCherryPickOf()) {
       change.setCherryPickOf(patchSetIdConverter.fromProto(proto.getCherryPickOf()));
     }
+    if (proto.hasServerId()) {
+      change.setServerId(proto.getServerId());
+    }
     return change;
   }
 
diff --git a/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
new file mode 100644
index 0000000..6e8c907
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Comment.Range;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.HumanComment.InFilePosition;
+import com.google.gerrit.proto.Entities.HumanComment.InFilePosition.Side;
+import com.google.protobuf.Parser;
+import java.time.Instant;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Proto converter between {@link HumanComment} and {@link
+ * com.google.gerrit.proto.Entities.HumanComment}.
+ */
+@Immutable
+public enum HumanCommentProtoConverter
+    implements ProtoConverter<Entities.HumanComment, HumanComment> {
+  INSTANCE;
+
+  private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Entities.ObjectId, ObjectId> objectIdConverter =
+      ObjectIdProtoConverter.INSTANCE;
+
+  @Override
+  public Entities.HumanComment toProto(HumanComment val) {
+
+    Entities.HumanComment.Builder res =
+        Entities.HumanComment.newBuilder()
+            .setPatchsetId(val.key.patchSetId)
+            .setAccountId(accountIdConverter.toProto(val.author.getId()))
+            .setCommentUuid(val.key.uuid)
+            .setCommentText(val.message)
+            .setUnresolved(val.unresolved)
+            .setWrittenOnMillis(val.writtenOn.toInstant().toEpochMilli())
+            .setServerId(val.serverId);
+    if (!val.key.filename.equals(PATCHSET_LEVEL)) {
+      InFilePosition.Builder inFilePos =
+          InFilePosition.newBuilder()
+              .setFilePath(val.key.filename)
+              .setSide(val.side <= 0 ? Side.PARENT : Side.REVISION);
+      if (val.range != null) {
+        inFilePos.setPositionRange(
+            InFilePosition.Range.newBuilder()
+                .setStartLine(val.range.startLine)
+                .setStartChar(val.range.startChar)
+                .setEndLine(val.range.endLine)
+                .setEndChar(val.range.endChar));
+      }
+      if (val.lineNbr != 0) {
+        inFilePos.setLineNumber(val.lineNbr);
+      }
+      res.setInFilePosition(inFilePos);
+    }
+
+    if (val.parentUuid != null) {
+      res.setParentCommentUuid(val.parentUuid);
+    }
+    if (val.tag != null) {
+      res.setTag(val.tag);
+    }
+    if (val.realAuthor != null) {
+      res.setRealAuthor(accountIdConverter.toProto(val.realAuthor.getId()));
+    }
+    if (val.getCommitId() != null) {
+      res.setDestCommitId(objectIdConverter.toProto(val.getCommitId()));
+    }
+
+    return res.build();
+  }
+
+  @Override
+  public HumanComment fromProto(Entities.HumanComment proto) {
+    Optional<InFilePosition> optInFilePosition =
+        proto.hasInFilePosition() ? Optional.of(proto.getInFilePosition()) : Optional.empty();
+    Comment.Key key =
+        new Comment.Key(
+            proto.getCommentUuid(),
+            optInFilePosition.isPresent() ? optInFilePosition.get().getFilePath() : PATCHSET_LEVEL,
+            proto.getPatchsetId());
+    HumanComment res =
+        new HumanComment(
+            key,
+            accountIdConverter.fromProto(proto.getAccountId()),
+            Instant.ofEpochMilli(proto.getWrittenOnMillis()),
+            optInFilePosition.isPresent()
+                ? (short) optInFilePosition.get().getSide().getNumber()
+                : Side.REVISION_VALUE,
+            proto.getCommentText(),
+            proto.getServerId(),
+            proto.getUnresolved());
+
+    res.parentUuid = proto.hasParentCommentUuid() ? proto.getParentCommentUuid() : null;
+    res.tag = proto.hasTag() ? proto.getTag() : null;
+    if (proto.hasRealAuthor()) {
+      res.realAuthor = new Comment.Identity(accountIdConverter.fromProto(proto.getRealAuthor()));
+    }
+    if (proto.hasDestCommitId()) {
+      res.setCommitId(objectIdConverter.fromProto(proto.getDestCommitId()));
+    }
+
+    optInFilePosition.ifPresent(
+        inFilePosition -> {
+          if (inFilePosition.hasPositionRange()) {
+            var range = inFilePosition.getPositionRange();
+            res.range =
+                new Range(
+                    range.getStartLine(),
+                    range.getStartChar(),
+                    range.getEndLine(),
+                    range.getEndChar());
+          }
+          if (inFilePosition.hasLineNumber()) {
+            res.lineNbr = inFilePosition.getLineNumber();
+          }
+        });
+    return res;
+  }
+
+  @Override
+  public Parser<Entities.HumanComment> getParser() {
+    return Entities.HumanComment.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java b/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java
new file mode 100644
index 0000000..11f78a4
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link MergeInput} and {@link
+ * com.google.gerrit.proto.Entities.MergeInput}.
+ */
+@Immutable
+public enum MergeInputProtoConverter implements ProtoConverter<Entities.MergeInput, MergeInput> {
+  INSTANCE;
+
+  @Override
+  public Entities.MergeInput toProto(MergeInput mergeInput) {
+    Entities.MergeInput.Builder builder = Entities.MergeInput.newBuilder();
+    if (mergeInput.source != null) {
+      builder.setSource(mergeInput.source);
+    }
+    if (mergeInput.sourceBranch != null) {
+      builder.setSourceBranch(mergeInput.sourceBranch);
+    }
+    if (mergeInput.strategy != null) {
+      builder.setStrategy(mergeInput.strategy);
+    }
+    builder.setAllowConflicts(mergeInput.allowConflicts);
+    return builder.build();
+  }
+
+  @Override
+  public MergeInput fromProto(Entities.MergeInput proto) {
+    MergeInput mergeInput = new MergeInput();
+    if (proto.hasSource()) {
+      mergeInput.source = proto.getSource();
+    }
+    if (proto.hasSourceBranch()) {
+      mergeInput.sourceBranch = proto.getSourceBranch();
+    }
+    if (proto.hasStrategy()) {
+      mergeInput.strategy = proto.getStrategy();
+    }
+    if (proto.hasAllowConflicts()) {
+      mergeInput.allowConflicts = proto.getAllowConflicts();
+    }
+    return mergeInput;
+  }
+
+  @Override
+  public Parser<Entities.MergeInput> getParser() {
+    return Entities.MergeInput.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
new file mode 100644
index 0000000..201dd78
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link NotifyInfo} and {@link
+ * com.google.gerrit.proto.Entities.NotifyInfo}.
+ */
+@Immutable
+public enum NotifyInfoProtoConverter implements ProtoConverter<Entities.NotifyInfo, NotifyInfo> {
+  INSTANCE;
+
+  @Override
+  public Entities.NotifyInfo toProto(NotifyInfo notifyInfo) {
+    Entities.NotifyInfo.Builder builder = Entities.NotifyInfo.newBuilder();
+    if (notifyInfo.accounts != null) {
+      builder.addAllAccounts(notifyInfo.accounts);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public NotifyInfo fromProto(Entities.NotifyInfo proto) {
+    return new NotifyInfo(proto.getAccountsList());
+  }
+
+  @Override
+  public Parser<Entities.NotifyInfo> getParser() {
+    return Entities.NotifyInfo.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index a3b4abf..196deca 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
 import java.time.Instant;
-import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Immutable
@@ -45,7 +44,7 @@
             .setRealUploaderAccountId(accountIdConverter.toProto(patchSet.realUploader()))
             .setCreatedOn(patchSet.createdOn().toEpochMilli());
     patchSet.branch().ifPresent(builder::setBranch);
-    List<String> groups = patchSet.groups();
+    ImmutableList<String> groups = patchSet.groups();
     if (!groups.isEmpty()) {
       builder.setGroups(PatchSet.joinGroups(groups));
     }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/entities/converter/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/entities/converter/package-info.java
index 0709b86..51ea401 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/entities/converter/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.entities.converter;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/entities/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/entities/package-info.java
index 0709b86..f1cde1b 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/entities/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.entities;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
index 873b659..b31d12e 100644
--- a/java/com/google/gerrit/exceptions/BUILD
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -7,5 +7,6 @@
     deps = [
         "//java/com/google/gerrit/entities",
         "//lib:jgit",
+        "//lib/errorprone:annotations",
     ],
 )
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/exceptions/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/exceptions/package-info.java
index 0709b86..ce52b4a 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/exceptions/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.exceptions;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/annotations/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/annotations/package-info.java
index 0709b86..f338b5b 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/annotations/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.annotations;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/access/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/access/package-info.java
index 0709b86..65e2d75 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/access/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.access;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 0d019aa..e40f82e 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -44,18 +45,22 @@
 
   GeneralPreferencesInfo getPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException;
 
   DiffPreferencesInfo getDiffPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
 
   EditPreferencesInfo getEditPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException;
 
   List<ProjectWatchInfo> getWatchedProjects() throws RestApiException;
 
+  @CanIgnoreReturnValue
   List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException;
 
   void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException;
@@ -72,6 +77,7 @@
 
   void deleteEmail(String email) throws RestApiException;
 
+  @CanIgnoreReturnValue
   EmailApi createEmail(EmailInput emailInput) throws RestApiException;
 
   EmailApi email(String email) throws RestApiException;
@@ -82,12 +88,14 @@
 
   List<SshKeyInfo> listSshKeys() throws RestApiException;
 
+  @CanIgnoreReturnValue
   SshKeyInfo addSshKey(String key) throws RestApiException;
 
   void deleteSshKey(int seq) throws RestApiException;
 
   Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
 
+  @CanIgnoreReturnValue
   Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove) throws RestApiException;
 
   GpgKeyApi gpgKey(String id) throws RestApiException;
@@ -102,6 +110,7 @@
 
   void deleteExternalIds(List<String> externalIds) throws RestApiException;
 
+  @CanIgnoreReturnValue
   List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
       throws RestApiException;
 
@@ -122,6 +131,7 @@
    * @param httpPassword the new password, {@code null} to remove the password.
    * @return the new password, {@code null} if the password was removed.
    */
+  @CanIgnoreReturnValue
   String setHttpPassword(String httpPassword) throws RestApiException;
 
   void delete() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 285b385..ad0d385 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -53,9 +54,11 @@
   AccountApi self() throws RestApiException;
 
   /** Create a new account with the given username and default options. */
+  @CanIgnoreReturnValue
   AccountApi create(String username) throws RestApiException;
 
   /** Create a new account. */
+  @CanIgnoreReturnValue
   AccountApi create(AccountInput input) throws RestApiException;
 
   /**
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/accounts/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/accounts/package-info.java
index 0709b86..5bd477a 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/accounts/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.accounts;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 14e1805..dec3125 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
@@ -25,6 +26,7 @@
 import com.google.gerrit.extensions.common.ChangeInfoDifference;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.CommitMessageInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
@@ -135,6 +137,7 @@
    *
    * @see Changes#id(String, int)
    */
+  @CanIgnoreReturnValue
   default ChangeApi revert() throws RestApiException {
     return revert(new RevertInput());
   }
@@ -144,17 +147,22 @@
    *
    * @see Changes#id(String, int)
    */
+  @CanIgnoreReturnValue
   ChangeApi revert(RevertInput in) throws RestApiException;
 
+  @CanIgnoreReturnValue
   default RevertSubmissionInfo revertSubmission() throws RestApiException {
     return revertSubmission(new RevertInput());
   }
 
+  @CanIgnoreReturnValue
   RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException;
 
   /** Create a merge patch set for the change. */
+  @CanIgnoreReturnValue
   ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
 
+  @CanIgnoreReturnValue
   ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException;
 
   default List<ChangeInfo> submittedTogether() throws RestApiException {
@@ -187,6 +195,7 @@
    * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
    *     chain
    */
+  @CanIgnoreReturnValue
   default Response<RebaseChainInfo> rebaseChain() throws RestApiException {
     return rebaseChain(new RebaseInput());
   }
@@ -197,6 +206,7 @@
    * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
    *     chain
    */
+  @CanIgnoreReturnValue
   Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException;
 
   /** Deletes a change. */
@@ -208,12 +218,14 @@
 
   IncludedInInfo includedIn() throws RestApiException;
 
+  @CanIgnoreReturnValue
   default ReviewerResult addReviewer(String reviewer) throws RestApiException {
     ReviewerInput in = new ReviewerInput();
     in.reviewer = reviewer;
     return addReviewer(in);
   }
 
+  @CanIgnoreReturnValue
   ReviewerResult addReviewer(ReviewerInput in) throws RestApiException;
 
   SuggestedReviewersRequest suggestReviewers() throws RestApiException;
@@ -318,6 +330,8 @@
    */
   ChangeEditApi edit() throws RestApiException;
 
+  CommitMessageInfo getMessage() throws RestApiException;
+
   /** Create a new patch set with a new commit message. */
   default void setMessage(String message) throws RestApiException {
     CommitMessageInput in = new CommitMessageInput();
@@ -356,6 +370,7 @@
   AttentionSetApi attention(String id) throws RestApiException;
 
   /** Adds a user to the attention set. */
+  @CanIgnoreReturnValue
   AccountInfo addToAttentionSet(AttentionSetInput input) throws RestApiException;
 
   /**
@@ -470,9 +485,11 @@
   void index() throws RestApiException;
 
   /** Check if this change is a pure revert of the change stored in revertOf. */
+  @CanIgnoreReturnValue
   PureRevertInfo pureRevert() throws RestApiException;
 
   /** Check if this change is a pure revert of claimedOriginal (SHA1 in 40 digit hex). */
+  @CanIgnoreReturnValue
   PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException;
 
   /**
@@ -711,6 +728,11 @@
     }
 
     @Override
+    public CommitMessageInfo getMessage() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void setMessage(CommitMessageInput in) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
index a26068a..2fd8a07 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -196,6 +196,18 @@
   void modifyCommitMessage(String newCommitMessage) throws RestApiException;
 
   /**
+   * Updates the author/committer of the change edit. If the change edit doesn't exist, it will be
+   * created based on the current patch set of the change.
+   *
+   * @param name the name of the author/committer
+   * @param email the email of the author/committer
+   * @param type the type of the identity being edited
+   * @throws RestApiException if the author/committer identity couldn't be updated
+   */
+  void modifyIdentity(String name, String email, ChangeEditIdentityType type)
+      throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
    */
@@ -269,5 +281,11 @@
     public void modifyCommitMessage(String newCommitMessage) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void modifyIdentity(String name, String email, ChangeEditIdentityType type)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java
index 0709b86..77d3f2e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.extensions.api.changes;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+public enum ChangeEditIdentityType {
+  AUTHOR,
+  COMMITTER
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/Changes.java b/java/com/google/gerrit/extensions/api/changes/Changes.java
index 9f70776..605a92e 100644
--- a/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -76,6 +77,7 @@
    */
   ChangeApi id(String project, int id) throws RestApiException;
 
+  @CanIgnoreReturnValue
   ChangeApi create(ChangeInput in) throws RestApiException;
 
   ChangeInfo createAsInfo(ChangeInput in) throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index 889175e..9b5e1da 100644
--- a/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -30,6 +31,7 @@
    *
    * @return the comment with its message updated.
    */
+  @CanIgnoreReturnValue
   CommentInfo delete(DeleteCommentInput input) throws RestApiException;
 
   /**
diff --git a/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index fa663a5..50816b7 100644
--- a/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface DraftApi extends CommentApi {
+  @CanIgnoreReturnValue
   CommentInfo update(DraftInput in) throws RestApiException;
 
   void delete() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java b/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
index 98ef31c..f8a769c 100644
--- a/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
+++ b/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
@@ -15,8 +15,18 @@
 package com.google.gerrit.extensions.api.changes;
 
 public enum NotifyHandling {
-  NONE,
-  OWNER,
-  OWNER_REVIEWERS,
-  ALL
+  NONE(0),
+  OWNER(1),
+  OWNER_REVIEWERS(2),
+  ALL(3);
+
+  private final int value;
+
+  NotifyHandling(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RecipientType.java b/java/com/google/gerrit/extensions/api/changes/RecipientType.java
index 3ddc597..91b2706 100644
--- a/java/com/google/gerrit/extensions/api/changes/RecipientType.java
+++ b/java/com/google/gerrit/extensions/api/changes/RecipientType.java
@@ -15,7 +15,17 @@
 package com.google.gerrit.extensions.api.changes;
 
 public enum RecipientType {
-  TO,
-  CC,
-  BCC
+  TO(0),
+  CC(1),
+  BCC(2);
+
+  private final int value;
+
+  RecipientType(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 98807cb..2584448 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -39,7 +38,7 @@
 
   public Map<String, Short> labels;
   public Map<String, List<CommentInput>> comments;
-  public Map<String, List<RobotCommentInput>> robotComments;
+  @Deprecated public Map<String, List<RobotCommentInput>> robotComments;
 
   /**
    * How to process draft comments already in the database that were not also described in this
@@ -115,12 +114,12 @@
     public Boolean unresolved;
   }
 
+  @Deprecated
   public static class RobotCommentInput extends Comment {
     public String robotId;
     public String robotRunId;
     public String url;
     public Map<String, String> properties;
-    public List<FixSuggestionInfo> fixSuggestions;
   }
 
   @CanIgnoreReturnValue
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 73fc170..69cf25d 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.api.changes;
 
 import com.google.common.collect.ListMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ArchiveFormat;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -44,24 +45,30 @@
 
   void description(String description) throws RestApiException;
 
+  @CanIgnoreReturnValue
   ReviewResult review(ReviewInput in) throws RestApiException;
 
+  @CanIgnoreReturnValue
   default ChangeInfo submit() throws RestApiException {
     SubmitInput in = new SubmitInput();
     return submit(in);
   }
 
+  @CanIgnoreReturnValue
   ChangeInfo submit(SubmitInput in) throws RestApiException;
 
+  @CanIgnoreReturnValue
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
 
   ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
 
+  @CanIgnoreReturnValue
   default ChangeApi rebase() throws RestApiException {
     RebaseInput in = new RebaseInput();
     return rebase(in);
   }
 
+  @CanIgnoreReturnValue
   ChangeApi rebase(RebaseInput in) throws RestApiException;
 
   ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException;
@@ -117,6 +124,7 @@
    * @param fixId the ID of the fix which should be applied
    * @throws RestApiException if the fix couldn't be applied
    */
+  @CanIgnoreReturnValue
   EditInfo applyFix(String fixId) throws RestApiException;
 
   /**
@@ -126,6 +134,7 @@
    * @param applyProvidedFixInput The fix(es) to apply to a new change edit.
    * @throws RestApiException if the fix couldn't be applied.
    */
+  @CanIgnoreReturnValue
   EditInfo applyFix(ApplyProvidedFixInput applyProvidedFixInput) throws RestApiException;
 
   Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException;
@@ -133,6 +142,7 @@
   Map<String, DiffInfo> getFixPreview(ApplyProvidedFixInput applyProvidedFixInput)
       throws RestApiException;
 
+  @CanIgnoreReturnValue
   DraftApi createDraft(DraftInput in) throws RestApiException;
 
   DraftApi draft(String id) throws RestApiException;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/changes/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/changes/package-info.java
index 0709b86..f548212 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/changes/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.changes;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/config/ExperimentApi.java b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
new file mode 100644
index 0000000..fb30884
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
@@ -0,0 +1,30 @@
+// Copyright (C) 20124 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface ExperimentApi {
+  ExperimentInfo get() throws RestApiException;
+
+  class NotImplemented implements ExperimentApi {
+    @Override
+    public ExperimentInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 8b69ded..26806d1 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.extensions.api.config;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ExperimentInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -31,20 +34,42 @@
 
   GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in) throws RestApiException;
 
   DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
 
   EditPreferencesInfo getDefaultEditPreferences() throws RestApiException;
 
+  @CanIgnoreReturnValue
   EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in) throws RestApiException;
 
   ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
 
   List<TopMenu.MenuEntry> topMenus() throws RestApiException;
 
+  ExperimentApi experiment(String name) throws RestApiException;
+
+  ListExperimentsRequest listExperiments() throws RestApiException;
+
+  abstract class ListExperimentsRequest {
+    private boolean enabledOnly;
+
+    public abstract ImmutableMap<String, ExperimentInfo> get() throws RestApiException;
+
+    public ListExperimentsRequest enabledOnly() {
+      enabledOnly = true;
+      return this;
+    }
+
+    public boolean getEnabledOnly() {
+      return enabledOnly;
+    }
+  }
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -102,5 +127,15 @@
     public List<TopMenu.MenuEntry> topMenus() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public ExperimentApi experiment(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListExperimentsRequest listExperiments() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/config/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/config/package-info.java
index 0709b86..2ee371c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/config/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.config;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/groups/Groups.java b/java/com/google/gerrit/extensions/api/groups/Groups.java
index 1a46930..8d53af0 100644
--- a/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.groups;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -42,9 +43,11 @@
   GroupApi id(String id) throws RestApiException;
 
   /** Create a new group with the given name and default options. */
+  @CanIgnoreReturnValue
   GroupApi create(String name) throws RestApiException;
 
   /** Create a new group. */
+  @CanIgnoreReturnValue
   GroupApi create(GroupInput input) throws RestApiException;
 
   /** Returns new request for listing groups. */
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/groups/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/groups/package-info.java
index 0709b86..3322f5c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/groups/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.groups;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/lfs/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/lfs/package-info.java
index 0709b86..2246f8d 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/lfs/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.lfs;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/package-info.java
index 0709b86..b7ad7da 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/plugins/Plugins.java b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
index 6c2d6db..fed8507 100644
--- a/java/com/google/gerrit/extensions/api/plugins/Plugins.java
+++ b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.plugins;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -29,9 +30,11 @@
   PluginApi name(String name) throws RestApiException;
 
   @Deprecated
+  @CanIgnoreReturnValue
   PluginApi install(String name, com.google.gerrit.extensions.common.InstallPluginInput input)
       throws RestApiException;
 
+  @CanIgnoreReturnValue
   PluginApi install(String name, InstallPluginInput input) throws RestApiException;
 
   abstract class ListRequest {
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/plugins/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/plugins/package-info.java
index 0709b86..c5324d6 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/plugins/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.plugins;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index 90f1f3f..a410205 100644
--- a/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.api.changes.ChangeApi.SuggestedReviewersRequest;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -21,6 +22,7 @@
 import java.util.List;
 
 public interface BranchApi {
+  @CanIgnoreReturnValue
   BranchApi create(BranchInput in) throws RestApiException;
 
   BranchInfo get() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigValue.java b/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
index 5d6d2b0..2f60628 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
@@ -17,6 +17,13 @@
 import java.util.List;
 
 public class ConfigValue {
+
+  public ConfigValue() {}
+
+  public ConfigValue(String value) {
+    this.value = value;
+  }
+
   public String value;
   public List<String> values;
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
index 975a57e..a5e23f2 100644
--- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
@@ -21,10 +22,12 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface LabelApi {
+  @CanIgnoreReturnValue
   LabelApi create(LabelDefinitionInput input) throws RestApiException;
 
   LabelDefinitionInfo get() throws RestApiException;
 
+  @CanIgnoreReturnValue
   LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException;
 
   default void delete() throws RestApiException {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 370068e..0b1b6b0 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
@@ -21,6 +22,7 @@
 import com.google.gerrit.extensions.common.BatchLabelInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -43,6 +45,7 @@
 
   ProjectAccessInfo access() throws RestApiException;
 
+  @CanIgnoreReturnValue
   ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException;
 
   ChangeInfo accessChange(ProjectAccessInput p) throws RestApiException;
@@ -53,6 +56,7 @@
 
   ConfigInfo config() throws RestApiException;
 
+  @CanIgnoreReturnValue
   ConfigInfo config(ConfigInput in) throws RestApiException;
 
   Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
@@ -69,9 +73,11 @@
   abstract class ListRefsRequest<T extends RefInfo> {
     protected int limit;
     protected int start;
+    protected boolean descendingOrder;
     protected String substring;
     protected String regex;
     protected String nextPageToken;
+    protected ListTagSortOption sortBy = ListTagSortOption.REF;
 
     public abstract List<T> get() throws RestApiException;
 
@@ -85,6 +91,16 @@
       return this;
     }
 
+    public ListRefsRequest<T> withDescendingOrder(boolean descendingOrder) {
+      this.descendingOrder = descendingOrder;
+      return this;
+    }
+
+    public ListRefsRequest<T> withSortBy(ListTagSortOption sortBy) {
+      this.sortBy = sortBy;
+      return this;
+    }
+
     public ListRefsRequest<T> withNextPageToken(String token) {
       this.nextPageToken = token;
       return this;
@@ -108,6 +124,14 @@
       return start;
     }
 
+    public boolean getDescendingOrder() {
+      return descendingOrder;
+    }
+
+    public ListTagSortOption getSortBy() {
+      return sortBy;
+    }
+
     public String getNextPageToken() {
       return nextPageToken;
     }
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 34ca7d4..7c8ecca 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -46,6 +47,7 @@
    * @return API for accessing the newly-created project.
    * @throws RestApiException if an error occurred.
    */
+  @CanIgnoreReturnValue
   ProjectApi create(String name) throws RestApiException;
 
   /**
@@ -55,6 +57,7 @@
    * @return API for accessing the newly-created project.
    * @throws RestApiException if an error occurred.
    */
+  @CanIgnoreReturnValue
   ProjectApi create(ProjectInput in) throws RestApiException;
 
   ListRequest list();
diff --git a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
index a6e79db..29765c0 100644
--- a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInput;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -21,12 +22,14 @@
 
 public interface SubmitRequirementApi {
   /** Create a new submit requirement. */
+  @CanIgnoreReturnValue
   SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException;
 
   /** Get existing submit requirement. */
   SubmitRequirementInfo get() throws RestApiException;
 
   /** Update existing submit requirement. */
+  @CanIgnoreReturnValue
   SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException;
 
   /** Delete existing submit requirement. */
diff --git a/java/com/google/gerrit/extensions/api/projects/TagApi.java b/java/com/google/gerrit/extensions/api/projects/TagApi.java
index 39efeac..69c29df 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface TagApi {
+  @CanIgnoreReturnValue
   TagApi create(TagInput input) throws RestApiException;
 
   TagInfo get() throws RestApiException;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/api/projects/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/api/projects/package-info.java
index 0709b86..4fcfc28 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/api/projects/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.api.projects;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/auth/oauth/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/auth/oauth/package-info.java
index 0709b86..591ea67 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/auth/oauth/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.auth.oauth;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/client/ChangeStatus.java b/java/com/google/gerrit/extensions/client/ChangeStatus.java
index 83d5bd2..4111dc4 100644
--- a/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -31,7 +31,7 @@
    *   <li>{@link #ABANDONED} - when the Abandon action is used.
    * </ul>
    */
-  NEW,
+  NEW(0),
 
   /**
    * Change is closed, and submitted to its destination branch.
@@ -39,7 +39,7 @@
    * <p>Once a change has been merged, it cannot be further modified by adding a replacement patch
    * set. Draft comments however may be published, supporting a post-submit review.
    */
-  MERGED,
+  MERGED(1),
 
   /**
    * Change is closed, but was not submitted to its destination branch.
@@ -54,5 +54,15 @@
    *   <li>{@link #NEW} - when the Restore action is used.
    * </ul>
    */
-  ABANDONED
+  ABANDONED(2);
+
+  private final int value;
+
+  ChangeStatus(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index b8843d3..8a68236 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.extensions.client;
 
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
 
 public abstract class Comment {
@@ -49,6 +51,8 @@
    */
   public String commitId;
 
+  public List<FixSuggestionInfo> fixSuggestions;
+
   // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
@@ -151,13 +155,15 @@
           && Objects.equals(inReplyTo, c.inReplyTo)
           && Objects.equals(updated, c.updated)
           && Objects.equals(message, c.message)
-          && Objects.equals(commitId, c.commitId);
+          && Objects.equals(commitId, c.commitId)
+          && Objects.equals(fixSuggestions, c.fixSuggestions);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(patchSet, id, path, side, parent, line, range, inReplyTo, updated, message);
+    return Objects.hash(
+        patchSet, id, path, side, parent, line, range, inReplyTo, updated, message, fixSuggestions);
   }
 }
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 5c48aaf..1ed9793 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -142,6 +142,7 @@
   public List<MenuItem> my;
   public List<String> changeTable;
   public Boolean allowBrowserNotifications;
+  public Boolean allowSuggestCodeWhileCommenting;
   /**
    * The sidebar section that the user prefers to have open on the diff page, or "NONE" if all
    * sidebars should be closed.
@@ -296,6 +297,7 @@
     p.workInProgressByDefault = false;
     p.allowBrowserNotifications = true;
     p.diffPageSidebar = "NONE";
+    p.allowSuggestCodeWhileCommenting = true;
     return p;
   }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/client/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/client/package-info.java
index 0709b86..75bbfda 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/client/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.client;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 90c3a92..52127e4 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -116,6 +116,7 @@
   public Collection<ReviewerUpdateInfo> reviewerUpdates;
   public Collection<ChangeMessageInfo> messages;
 
+  public Integer currentRevisionNumber;
   public String currentRevision;
   public Map<String, RevisionInfo> revisions;
   public Boolean _moreChanges;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 51c35dc..308daf0 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -75,12 +75,13 @@
   @SuppressWarnings("unchecked") // reflection is used to construct instances of T
   private static <T> T getAdded(T oldValue, T newValue) {
     if (newValue instanceof Collection) {
-      List<?> result = getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
+      ImmutableList<?> result =
+          getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
       return (T) result;
     }
 
     if (newValue instanceof Map) {
-      Map<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
+      ImmutableMap<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
       return (T) result;
     }
 
diff --git a/java/com/google/gerrit/extensions/common/CommentInfo.java b/java/com/google/gerrit/extensions/common/CommentInfo.java
index 35587a0..aee2552 100644
--- a/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -39,13 +39,14 @@
       CommentInfo ci = (CommentInfo) o;
       return Objects.equals(author, ci.author)
           && Objects.equals(tag, ci.tag)
-          && Objects.equals(unresolved, ci.unresolved);
+          && Objects.equals(unresolved, ci.unresolved)
+          && Objects.equals(fixSuggestions, ci.fixSuggestions);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(super.hashCode(), author, tag, unresolved);
+    return Objects.hash(super.hashCode(), author, tag, unresolved, fixSuggestions);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/CommitMessageInfo.java b/java/com/google/gerrit/extensions/common/CommitMessageInfo.java
new file mode 100644
index 0000000..fdd7cc3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/CommitMessageInfo.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+
+/** Representation of a commit message used in the API. */
+public class CommitMessageInfo {
+  /**
+   * The subject of the change.
+   *
+   * <p>First line of the commit message.
+   */
+  public String subject;
+
+  /** Full commit message of the change. */
+  public String fullMessage;
+
+  /**
+   * The footers from the commit message.
+   *
+   * <p>Key-value pairs from the last paragraph of the commit message.
+   */
+  public Map<String, String> footers;
+}
diff --git a/java/com/google/gerrit/extensions/common/CommitMessageInput.java b/java/com/google/gerrit/extensions/common/CommitMessageInput.java
index 1e23cb4..0b27c6b 100644
--- a/java/com/google/gerrit/extensions/common/CommitMessageInput.java
+++ b/java/com/google/gerrit/extensions/common/CommitMessageInput.java
@@ -27,4 +27,6 @@
   @Nullable public NotifyHandling notify;
 
   public Map<RecipientType, NotifyInfo> notifyDetails;
+
+  public String committerEmail;
 }
diff --git a/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
index 6f0b178..2c2b866 100644
--- a/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
+++ b/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -18,6 +18,7 @@
 
 public class DownloadSchemeInfo {
   public String url;
+  public String description;
   public Boolean isAuthRequired;
   public Boolean isAuthSupported;
   public Map<String, String> commands;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/common/ExperimentInfo.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/common/ExperimentInfo.java
index 0709b86..0cf5c9d 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/common/ExperimentInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2034 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.extensions.common;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+public class ExperimentInfo {
+  /** Whether the experiment is enabled. */
+  public Boolean enabled;
+}
diff --git a/java/com/google/gerrit/extensions/common/FixReplacementInfo.java b/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
index 9e5890e..6df4fb9 100644
--- a/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
+++ b/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
@@ -15,9 +15,26 @@
 package com.google.gerrit.extensions.common;
 
 import com.google.gerrit.extensions.client.Comment;
+import java.util.Objects;
 
 public class FixReplacementInfo {
   public String path;
   public Comment.Range range;
   public String replacement;
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof FixReplacementInfo)) {
+      return false;
+    }
+    FixReplacementInfo fs = (FixReplacementInfo) o;
+    return Objects.equals(path, fs.path)
+        && Objects.equals(range, fs.range)
+        && Objects.equals(replacement, fs.replacement);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(path, range, replacement);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java b/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
index 7ba7fcc..50df366 100644
--- a/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
+++ b/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
@@ -15,9 +15,26 @@
 package com.google.gerrit.extensions.common;
 
 import java.util.List;
+import java.util.Objects;
 
 public class FixSuggestionInfo {
   public String fixId;
   public String description;
   public List<FixReplacementInfo> replacements;
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof FixSuggestionInfo)) {
+      return false;
+    }
+    FixSuggestionInfo fs = (FixSuggestionInfo) o;
+    return Objects.equals(fixId, fs.fixId)
+        && Objects.equals(description, fs.description)
+        && Objects.equals(replacements, fs.replacements);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(fixId, description, replacements);
+  }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/common/ListTagSortOption.java
index 0709b86..2140924 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.extensions.common;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+public enum ListTagSortOption {
+  REF,
+  CREATION_TIME,
+}
diff --git a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
index 8d8731f..780cab7 100644
--- a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.util.List;
 import java.util.Map;
 
+@Deprecated
 public class RobotCommentInfo extends CommentInfo {
   public String robotId;
   public String robotRunId;
   public String url;
   public Map<String, String> properties;
-  public List<FixSuggestionInfo> fixSuggestions;
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/common/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/common/package-info.java
index 0709b86..40f03b2 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/common/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.common;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 33cbb99..c126928 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -13,6 +13,7 @@
         "//lib:jgit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
index c34e439..049d6e4 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertAbout;
 import static com.google.gerrit.extensions.common.testing.AccountInfoSubject.accounts;
 import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.BooleanSubject;
 import com.google.common.truth.ComparableSubject;
@@ -26,6 +27,7 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.truth.ListSubject;
 import java.sql.Timestamp;
 import java.util.List;
@@ -52,6 +54,12 @@
     this.commentInfo = commentInfo;
   }
 
+  public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
+    return check("fixSuggestions")
+        .about(elements())
+        .thatCustom(commentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
+  }
+
   public StringSubject uuid() {
     return check("id").that(commentInfo().id);
   }
@@ -116,4 +124,8 @@
     isNotNull();
     return commentInfo;
   }
+
+  public FixSuggestionInfoSubject onlyFixSuggestion() {
+    return fixSuggestions().onlyElement();
+  }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/common/testing/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/common/testing/package-info.java
index 0709b86..869891a 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/common/testing/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.common.testing;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
index 9c354fb..b1c1e93 100644
--- a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
+++ b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.extensions.conditions;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import java.util.Collections;
 
 /** Delayed evaluation of a boolean condition. */
 public abstract class BooleanCondition {
@@ -270,8 +270,8 @@
     }
 
     @Override
-    public <T> Iterable<T> children(Class<T> type) {
-      return Collections.emptyList();
+    public <T> ImmutableList<T> children(Class<T> type) {
+      return ImmutableList.of();
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/conditions/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/conditions/package-info.java
index 0709b86..8a6b726 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/conditions/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.conditions;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/config/DownloadScheme.java b/java/com/google/gerrit/extensions/config/DownloadScheme.java
index 15801d4..5dad525 100644
--- a/java/com/google/gerrit/extensions/config/DownloadScheme.java
+++ b/java/com/google/gerrit/extensions/config/DownloadScheme.java
@@ -37,4 +37,9 @@
 
   /** Returns whether the download scheme is hidden in the UI */
   public abstract boolean isHidden();
+
+  /** Returns an optional description of the scheme */
+  public String getDescription() {
+    return null;
+  }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/config/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/config/package-info.java
index 0709b86..2ddb32c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/config/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.config;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/events/GerritEvent.java b/java/com/google/gerrit/extensions/events/GerritEvent.java
index e43a981..abab852 100644
--- a/java/com/google/gerrit/extensions/events/GerritEvent.java
+++ b/java/com/google/gerrit/extensions/events/GerritEvent.java
@@ -19,4 +19,8 @@
 /** Base interface to be extended by Events. */
 public interface GerritEvent {
   NotifyHandling getNotify();
+
+  default String getInstanceId() {
+    return null;
+  }
 }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/events/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/events/package-info.java
index 0709b86..e0a454c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/events/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.events;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/package-info.java
index 0709b86..f886030 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItem.java b/java/com/google/gerrit/extensions/registration/DynamicItem.java
index 3f848cb..4464af7 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.registration;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.inject.Binder;
 import com.google.inject.Key;
@@ -159,6 +160,7 @@
    * @param pluginName the name of the plugin providing the item.
    * @return handle to remove the item at a later point in time.
    */
+  @CanIgnoreReturnValue
   public RegistrationHandle set(T item, String pluginName) {
     return set(Providers.of(item), pluginName);
   }
@@ -170,6 +172,7 @@
    * @param pluginName name of the source providing the implementation.
    * @return handle to remove the item at a later point in time.
    */
+  @CanIgnoreReturnValue
   public RegistrationHandle set(Provider<T> impl, String pluginName) {
     final Extension<T> item = new Extension<>(pluginName, impl);
     Extension<T> old = null;
@@ -197,6 +200,7 @@
    * @param pluginName the name of the plugin providing the item.
    * @return a handle that can remove this item later, or hot-swap the item.
    */
+  @CanIgnoreReturnValue
   public ReloadableRegistrationHandle<T> set(Key<T> key, Provider<T> impl, String pluginName) {
     final Extension<T> item = new Extension<>(pluginName, impl);
     Extension<T> old = null;
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 6dc8c6a..9925a66 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.inject.Binder;
 import com.google.inject.Key;
@@ -249,6 +250,7 @@
    * @param item the item to add to the collection. Must not be null.
    * @return handle to remove the item at a later point in time.
    */
+  @CanIgnoreReturnValue
   public RegistrationHandle add(String pluginName, T item) {
     return add(pluginName, Providers.of(item));
   }
@@ -259,6 +261,7 @@
    * @param item the item to add to the collection. Must not be null.
    * @return handle to remove the item at a later point in time.
    */
+  @CanIgnoreReturnValue
   public RegistrationHandle add(String pluginName, Provider<T> item) {
     final AtomicReference<Extension<T>> ref =
         new AtomicReference<>(new Extension<>(pluginName, item));
@@ -281,6 +284,7 @@
    * @return a handle that can remove this item later, or hot-swap the item without it ever leaving
    *     the collection.
    */
+  @CanIgnoreReturnValue
   public ReloadableRegistrationHandle<T> add(String pluginName, Key<T> key, Provider<T> item) {
     AtomicReference<Extension<T>> ref = new AtomicReference<>(new Extension<>(pluginName, item));
     items.add(ref);
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index 67fc068..994a8fb 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -16,6 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.inject.Key;
@@ -51,6 +52,7 @@
    * @return a handle that can remove this item later, or hot-swap the item without it ever leaving
    *     the collection.
    */
+  @CanIgnoreReturnValue
   public ReloadableRegistrationHandle<T> put(String pluginName, Key<T> key, Provider<T> item) {
     requireNonNull(item);
     String exportName = ((Export) key.getAnnotation()).value();
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/registration/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/registration/package-info.java
index 0709b86..bcd5880 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/registration/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.registration;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 2ee376e..905c0db 100644
--- a/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
@@ -73,6 +74,7 @@
   }
 
   /** Set the MIME type of the result, and return {@code this}. */
+  @CanIgnoreReturnValue
   public BinaryResult setContentType(String contentType) {
     this.contentType = contentType != null ? contentType : OCTET_STREAM;
     return this;
@@ -84,6 +86,7 @@
   }
 
   /** Set the character set used to encode text data and return {@code this}. */
+  @CanIgnoreReturnValue
   public BinaryResult setCharacterEncoding(Charset encoding) {
     characterEncoding = encoding;
     return this;
@@ -95,6 +98,7 @@
   }
 
   /** Set the attachment file name and return {@code this}. */
+  @CanIgnoreReturnValue
   public BinaryResult setAttachmentName(String attachmentName) {
     this.attachmentName = attachmentName;
     return this;
@@ -106,6 +110,7 @@
   }
 
   /** Set the content length of the result; -1 if not known. */
+  @CanIgnoreReturnValue
   public BinaryResult setContentLength(long len) {
     this.contentLength = len;
     return this;
@@ -117,6 +122,7 @@
   }
 
   /** Disable gzip compression for already compressed responses. */
+  @CanIgnoreReturnValue
   public BinaryResult disableGzip() {
     this.gzip = false;
     return this;
@@ -128,6 +134,7 @@
   }
 
   /** Wrap the binary data in base64 encoding. */
+  @CanIgnoreReturnValue
   public BinaryResult base64() {
     base64 = true;
     return this;
diff --git a/java/com/google/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java
index 3ebae8d..85dd643 100644
--- a/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.restapi;
 
 import com.google.common.collect.ImmutableMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.concurrent.TimeUnit;
 
 /** Special return value to mean specific HTTP status codes in a REST API. */
@@ -90,6 +91,7 @@
 
   public abstract CacheControl caching();
 
+  @CanIgnoreReturnValue
   public abstract Response<T> caching(CacheControl c);
 
   @Override
diff --git a/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
index 85bd5a1..6d0191e 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
@@ -128,6 +129,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestReadView<P>> ScopedBindingBuilder to(Class<T> impl) {
       return binder.to(impl);
     }
@@ -136,11 +138,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestReadView<P>> ScopedBindingBuilder toProvider(
         Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestReadView<P>> ScopedBindingBuilder toProvider(
         Provider<? extends T> provider) {
       return binder.toProvider(provider);
@@ -154,6 +158,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestModifyView<P, ?>> ScopedBindingBuilder to(Class<T> impl) {
       return binder.to(impl);
     }
@@ -162,11 +167,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestModifyView<P, ?>> ScopedBindingBuilder toProvider(
         Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <T extends RestModifyView<P, ?>> ScopedBindingBuilder toProvider(
         Provider<? extends T> provider) {
       return binder.toProvider(provider);
@@ -180,6 +187,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
         ScopedBindingBuilder to(Class<T> impl) {
       return binder.to(impl);
@@ -190,11 +198,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
         ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
         ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
       return binder.toProvider(provider);
@@ -208,6 +218,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
         ScopedBindingBuilder to(Class<T> impl) {
       return binder.to(impl);
@@ -218,11 +229,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
         ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
         ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
       return binder.toProvider(provider);
@@ -236,6 +249,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
         ScopedBindingBuilder to(Class<T> impl) {
       return binder.to(impl);
@@ -246,11 +260,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
         ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
         ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
       return binder.toProvider(provider);
@@ -264,6 +280,7 @@
       this.binder = binder;
     }
 
+    @CanIgnoreReturnValue
     public <C extends RestResource, T extends ChildCollection<P, C>> ScopedBindingBuilder to(
         Class<T> impl) {
       return binder.to(impl);
@@ -273,11 +290,13 @@
       binder.toInstance(impl);
     }
 
+    @CanIgnoreReturnValue
     public <C extends RestResource, T extends ChildCollection<P, C>>
         ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
       return binder.toProvider(providerType);
     }
 
+    @CanIgnoreReturnValue
     public <C extends RestResource, T extends ChildCollection<P, C>>
         ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
       return binder.toProvider(provider);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/restapi/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/restapi/package-info.java
index 0709b86..f3762e5 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/restapi/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.restapi;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BUILD b/java/com/google/gerrit/extensions/restapi/testing/BUILD
index da11ce8..a86edb8 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BUILD
+++ b/java/com/google/gerrit/extensions/restapi/testing/BUILD
@@ -9,6 +9,7 @@
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/truth",
+        "//lib/errorprone:annotations",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/restapi/testing/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/restapi/testing/package-info.java
index 0709b86..74e31e7 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/restapi/testing/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.restapi.testing;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/systemstatus/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/systemstatus/package-info.java
index 0709b86..ed9a8cc 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/systemstatus/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.systemstatus;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/extensions/validators/package-info.java
similarity index 64%
copy from java/com/google/gerrit/server/StarredChangesUtil.java
copy to java/com/google/gerrit/extensions/validators/package-info.java
index 0709b86..076519a 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/extensions/validators/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+@CheckReturnValue
+package com.google.gerrit.extensions.validators;
 
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/webui/UiAction.java b/java/com/google/gerrit/extensions/webui/UiAction.java
index 9da0642..6c1d01d 100644
--- a/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.webui;