Merge "WorkQueue: Do not busy-reschedule canceled or done one-shot tasks"
diff --git a/.bazelversion b/.bazelversion
index deeb3d6..b13d146 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-9.2.0
+9.3.0
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index a2fecab..6535f6f 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -733,13 +733,25 @@
 by Gerrit. By granting `Push` without `Push Merge Commit`, the only
 merges that enter the system will be those created by Gerrit.
 
-The reference name connected to a `Push Merge Commit` entry must always
-be prefixed with `refs/for/`, for example `refs/for/refs/heads/BRANCH`.
-This applies even for an entry that complements a `Push` entry for
-`refs/heads/BRANCH` that allows direct pushes of non-merge commits, and
-the intention of the `Push Merge Commit` entry is to allow direct pushes
-of merge commits.
+For pushes for review, a `Push Merge Commit` entry must be prefixed with
+`refs/for/`, for example `refs/for/refs/heads/BRANCH`; granting it only on
+`refs/heads/BRANCH` does not authorize review uploads. For direct pushes, the
+permission should be granted on `refs/heads/BRANCH`; for legacy reasons an
+entry prefixed with `refs/for/` also continues to authorize direct pushes (the
+fallback applies to direct pushes only, not the other way around). Note that
+this fallback matches the ref pattern of the `refs/for/` entry, so a broad
+entry such as `refs/for/*` authorizes direct merge pushes to every matching
+branch.
 
+[NOTE]
+The `refs/for/` fallback for direct merge pushes is deprecated and will be
+removed in a future release. Grant `Push Merge Commit` on `refs/heads/BRANCH`
+for direct merge pushes. A direct merge push authorized only through the
+fallback logs a warning on the server.
+
+The `refs/for/` fallback above applies only to the direct-push merge check.
+The `skip-validation` push option's merge requirement is not subject to it: it
+requires `Push Merge Commit` on the destination ref itself.
 
 [[category_push_annotated]]
 [[category_create_annotated]]
@@ -1068,44 +1080,50 @@
 This category controls who is able to use AI-assisted review features,
 such as the "Review Agent" and "Create AI Review Prompt" buttons.
 
-[NOTE]
-Unlike all other Gerrit permissions which use a default-deny model
-(access is denied unless an explicit 'ALLOW' rule grants it), the
-`aiReview` permission uses a *default-allow* model during the experiment
-phase. This means:
+This permission follows the standard default-deny model: access is
+granted only to users matching an `ALLOW` rule (and not blocked by a
+`DENY` or `BLOCK` rule). To match the previous opt-out behavior,
+`All-Projects` ships with an explicit grant for `Registered Users` on
+`refs/heads/*`. Sites upgrading from an earlier release receive the same
+grant automatically via schema migration when an `aiReview` rule does
+not already exist on `refs/heads/*`. Admins can revoke, scope, or
+restrict the grant like any other change-scoped permission.
 
-* When no `aiReview` rules are configured, all users have access.
-* 'DENY' and 'BLOCK' rules can be used on their own to restrict specific
-  groups, without needing to first grant 'ALLOW' to everyone else.
-* When 'ALLOW' rules are present, the permission switches to the standard
-  model: only users matching an 'ALLOW' rule have access.
-* Site administrators are subject to the same rules as other users.
-
-This non-standard behavior will be removed when the experiment ends, at
-which point `aiReview` will follow the standard default-deny model like
-all other permissions.
-
-To restrict AI review to a specific group on all branches:
+To restrict AI review to a specific group on all branches, replace the
+default grant:
 
 ----
   [access "refs/heads/*"]
     aiReview = group AI Reviewers
 ----
 
-To deny a specific group while keeping access for everyone else:
+Because the default grant is a broad `allow` for `Registered Users`, a
+`DENY` or `BLOCK` rule in the same section does not carve a subgroup out
+of it. Within one access section an `ALLOW` overrides a `BLOCK`, and a
+broad `ALLOW` still grants a user who is also matched by a narrower
+`DENY`. Every member of a subgroup such as `Restricted Users` is also a
+`Registered User`, so the following does *not* remove their access:
 
 ----
   [access "refs/heads/*"]
+    aiReview = group Registered Users
     aiReview = deny group Restricted Users
 ----
 
-To block a group across all child projects (cannot be overridden):
+To exclude a subgroup, either grant `aiReview` only to the groups that
+should have it (replacing the broad grant, as shown above), or add a
+`BLOCK` for the subgroup in a child project or another access section,
+where the inherited `ALLOW` does not override it:
 
 ----
+  child-project: project.config
   [access "refs/heads/*"]
     aiReview = block group Restricted Users
 ----
 
+A `DENY` only revokes the grant for the same group, so it cannot exclude
+a subgroup while the broad grant stands.
+
 
 [[example_roles]]
 == Examples of typical roles in a project
diff --git a/Documentation/cmd-convert-ref-storage.txt b/Documentation/cmd-convert-ref-storage.txt
index aae385f..d5f5507 100644
--- a/Documentation/cmd-convert-ref-storage.txt
+++ b/Documentation/cmd-convert-ref-storage.txt
@@ -1,7 +1,7 @@
 = gerrit convert-ref-storage
 
 == NAME
-gerrit convert-ref-storage - Convert ref storage to reftable (experimental).
+gerrit convert-ref-storage - Convert ref storage to reftable.
 
 A reftable file is a portable binary file format customized for reference storage.
 References are sorted, enabling linear scans, binary search lookup, and range scans.
@@ -21,6 +21,10 @@
 == DESCRIPTION
 Convert ref storage to reftable.
 
+Converting to `reftable` additionally requires the server to run with
+link:config-gerrit.html#core.usePerRequestRefCache[core.usePerRequestRefCache]
+set to `false` (reftable is otherwise unservable); the command fails otherwise.
+
 == ACCESS
 Administrators
 
@@ -47,7 +51,7 @@
 
 Convert ref format for project "core" to reftable:
 ----
-$ ssh -p 29418 review.example.com gerrit convert-ref-format -p core
+$ ssh -p 29418 review.example.com gerrit convert-ref-storage -p core
 ----
 
 GERRIT
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index e07ac6f..043e65b 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -125,7 +125,7 @@
 	Close the specified SSH connection.
 
 link:cmd-convert-ref-storage.html[gerrit convert-ref-storage]::
-	Convert ref storage to reftable (experimental).
+	Convert ref storage to reftable.
 
 link:cmd-create-account.html[gerrit create-account]::
 	Create a new user account.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7d11a36..6521e3b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -60,8 +60,15 @@
 
 ---
 [accountPatchReviewDb]
-  url = jdbc:h2:file:/path/to/db
+  url = jdbc:h2:async:/path/to/db
 ---
++
+For legacy configurations, Gerrit also accepts the previous
+`jdbc:h2:file:/path/to/db` form as an alias for `jdbc:h2:async:/path/to/db`.
+H2 documents the `async:` file system as a workaround for applications that
+may interrupt embedded H2 operations, but not as a complete safety guarantee.
+Use a client/server database for the strongest isolation from interrupted
+request threads.
 
 
 [[accountPatchReviewDb.poolLimit]]accountPatchReviewDb.poolLimit::
@@ -761,6 +768,25 @@
 +
 Existing tokens will not be affected by changing this value.
 
+[[auth.token-expiry.notify]]auth.token-expiry.notify::
++
+Whether to send out email notifications when an authentication token is about
+to expire. Notification intervals can be configured using
+link:#.token-expiry.notifyDaysBefore[`.token-expiry.notifyDaysBefore`].
++
+Defaults to `true`.
+
+[[auth.token-expiry.notifyDaysBefore]]auth.token-expiry.notifyDaysBefore::
++
+A number of days before token expiration on which to send notification
+emails to users. Can be provided multiple times to send multiple
+notifications. Negative values will be ignored.
++
+The notification system runs daily at midnight (00:00) and sends at most one
+email per token per day.
++
+Defaults to 0, 7, 14 and 21 days, if no value is set.
+
 [[auth.httpPasswordFallbackEnabled]]auth.httpPasswordFallbackEnabled::
 +
 To provide backward compatibility Gerrit will automatically fall back to
@@ -2651,6 +2677,10 @@
 (lazily) if needed. This helps reduce the overhead of checking if
 the packed-refs file is outdated.
 +
+Repositories using the reftable format cannot be served while this cache
+is enabled. Set this option to `false` and restart Gerrit before serving
+reftable repositories.
++
 Default is `true`.
 
 [[core.useFileKeyByProjectCache]]core.useFileKeyByProjectCache::
@@ -3622,7 +3652,8 @@
 `X-Forwarded-Server`.  These headers are typically set by Apache's
 link:https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers[mod_proxy,role=external,window=_blank].
 +
-[NOTE]
+[[httpd.listenUrl.proxyHttpSecurity]]
+[WARNING]
 --
 For security reasons, make sure to only allow connections from a
 trusted reverse proxy in your network, as clients could otherwise
@@ -3695,6 +3726,27 @@
 By default, `\http://*:8080`.
 --
 
+[[httpd.enableProxyProtocol]]httpd.enableProxyProtocol::
++
+If `true`, enables parsing PROXY protocol headers on incoming HTTP connections
+configured with `proxy-http://` or `proxy-https://`. This allows Gerrit to
+access the original client address and port instead of the reverse proxy
+address. A PROXY protocol header is optional; connections without one are
+handled normally.
+If both a PROXY protocol header and `X-Forwarded-For` are present, the
+`X-Forwarded-For` header takes precedence for the client address.
++
+[WARNING]
+This should only be enabled when *all* HTTP traffic on the listener comes
+from trusted proxies. A client that can connect directly to the HTTP
+listener can send a forged PROXY protocol header and spoof its source
+address.
+
+See also <<httpd.listenUrl.proxyHttpSecurity, the proxy-http security warning>>
+for related reverse-proxy security considerations.
++
+By default, `false`.
+
 [[httpd.reuseAddress]]httpd.reuseAddress::
 +
 If `true`, permits the daemon to bind to the port even if the port
@@ -3991,14 +4043,6 @@
 using the link:pgm-reindex.html[reindex program] before restarting the
 Gerrit server.
 
-[[index.directory]]index.directory::
-+
-Path to the directory where Gerrit stores its index.
-+
-If not absolute, the path is resolved relative to `$site_path`.
-+
-Default is `$site_path/index`.
-
 [[index.type]]index.type::
 +
 *(DEPRECATED)* The only supported value is `LUCENE`, which is the default,
@@ -4008,6 +4052,14 @@
 For using other indexing backends (e.g. ElasticSearch), refer to
 `gerrit.installIndexModule` setting.
 +
+[[index.directory]]index.directory::
++
+Path to the directory where Gerrit stores its secondary index files.
++
+If not absolute, the path is resolved relative to `$site_path`.
++
+Default is `$site_path/index`.
++
 [[index.threads]]index.threads::
 +
 Number of threads to use for indexing in normal interactive operations. Setting
@@ -4626,6 +4678,23 @@
 [experiments]
   enabled = ExperimentKey
 ----
++
+For example, `GerritBackendFeature__allow_ai_review_for_registered_users`
+grants the `aiReview` change permission to all registered users even when
+no explicit `aiReview` rule is configured, letting a site enable AI Review
+before the permission is added to `project.config`:
++
+----
+[experiments]
+  enabled = GerritBackendFeature__allow_ai_review_for_registered_users
+----
++
+NOTE: While this experiment is enabled, any identified user can use AI
+Review regardless of `project.config`. The experiment is an unconditional
+override that ignores ACLs: an explicit `block aiReview` rule has no
+effect, so AI Review can no longer be disabled or restricted by ACL.
+Disable the experiment and grant `aiReview` explicitly if you need
+per-ref or per-group control.
 
 [[experiments.disabled]]experiments.disabled::
 +
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index ed63caa..54c189b 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -82,6 +82,21 @@
 current revision displayed, an instance of
 link:rest-api-changes.html#revision-info[RevisionInfo]
 
+=== chat-panel
+The `chat-panel` extension point wraps the `<chat-panel>` component in the
+change view's right-hand sidebar. Plugins can decorate or replace the default
+chat panel.
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
 === change-metadata-item
 The `change-metadata-item` extension point is located on the change view
 left panel, below the `Submit Requirements` and `Links` sections by default.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index cb217fb..53af781 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7047,6 +7047,10 @@
 The `base` parameter can be specified to control the base patch set from which
 the blame should be generated.
 
+If a `.git-blame-ignore-revs` file is present at the root of the repository in
+the revision being blamed, the revisions listed in it are ignored when computing
+the blame.
+
 [[set-reviewed]]
 === Set Reviewed
 --
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 4d1a6b4..c2ac578 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -941,7 +941,7 @@
     "GerritBackendFeature__allow_fix_suggestions_in_comments": {
       "enabled": false
     },
-    "GerritBackendFeature__attach_nonce_to_documentation": {
+    "GerritBackendFeature__skip_submit_records_without_submit_requirements": {
       "enabled": true
     }
   }
@@ -2119,7 +2119,7 @@
 
 .Request
 ----
-  GET /config/server/experiments/mGerritBackendFeature__attach_nonce_to_documentation HTTP/1.0
+  GET /config/server/experiments/GerritBackendFeature__allow_fix_suggestions_in_comments HTTP/1.0
 ----
 
 As response an link:#experiment-info[Experiment] entity is returned that
diff --git a/MODULE.bazel b/MODULE.bazel
index 04e95b1..b02c014 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -4,7 +4,7 @@
 bazel_dep(name = "bazel_features", version = "1.51.0")
 bazel_dep(name = "bazel_lib", version = "3.7.2")
 bazel_dep(name = "platforms", version = "1.1.0")
-bazel_dep(name = "rules_jvm_external", version = "7.1")
+bazel_dep(name = "rules_jvm_external", version = "7.2")
 
 # Language rules.
 bazel_dep(name = "aspect_rules_js", version = "3.4.1")
@@ -29,6 +29,16 @@
 # Libraries / toolchains.
 bazel_dep(name = "protobuf", version = "36.2")
 
+# TODO(davido): Gerrit uses Java protobufs only. Disable Protobuf's transitive
+# Rust setup until upstream avoids resolving Cargo/Rust tools for non-Rust consumers.
+# https://github.com/protocolbuffers/protobuf/issues/30684
+single_version_override(
+    module_name = "protobuf",
+    patch_strip = 1,
+    patches = ["//tools/patches:protobuf-36.2-no-rust.patch"],
+    version = "36.2",
+)
+
 # In-tree modules.
 bazel_dep(name = "jgit")
 local_path_override(
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 0bfdc41..9f8c9c5 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -2,28 +2,20 @@
   "lockFileVersion": 28,
   "registryFileHashes": {
     "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
-    "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
-    "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
     "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
-    "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
     "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
-    "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
     "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
-    "https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5",
     "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
     "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
     "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2",
     "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c",
     "https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a",
     "https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b",
-    "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
     "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
-    "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e",
-    "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
-    "https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8",
     "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4",
     "https://bcr.bazel.build/modules/apple_support/2.3.0/MODULE.bazel": "d48f824ae8eeea5f837eb3038cef3615075d996d3e82eb9187192c2820605a81",
-    "https://bcr.bazel.build/modules/apple_support/2.3.0/source.json": "d99b0a50918c4484856d773026b2fd60a694668c70fc0e19d592786dc7e5e469",
+    "https://bcr.bazel.build/modules/apple_support/2.5.2/MODULE.bazel": "bb1b7081e390a8d3885f642cb0073dfcf8e7a36525e079fa8f5835af7079c0fe",
+    "https://bcr.bazel.build/modules/apple_support/2.5.2/source.json": "dd51ee925a550e92b55eb41c4c809a967da311a2d2520241252a238a48b39c4c",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.2/MODULE.bazel": "30dfabbfae0139b1f0036e01c201dd4c0167da3017f0b7ef3820d78e07622989",
@@ -43,7 +35,6 @@
     "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.5.1/source.json": "dbe0a75bf1f741b18c7b72dd4d205fa5a45c8c135fee15f7fdd1a2b50ba0fdde",
     "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
     "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
-    "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
     "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
     "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df",
     "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
@@ -51,13 +42,10 @@
     "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
     "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
     "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
-    "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a",
     "https://bcr.bazel.build/modules/bazel_features/1.25.0/MODULE.bazel": "e2e60a10a6da64bbf533f15ca652bf61a033e41c2ed734d79a9a08ba87f68c1a",
     "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
     "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
-    "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
     "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
-    "https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc",
     "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
     "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e",
     "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3",
@@ -85,7 +73,6 @@
     "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
     "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
     "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
-    "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
     "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.0/MODULE.bazel": "2fb3fb53675f6adfc1ca5bfbd5cfb655ae350fba4706d924a8ec7e3ba945671c",
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
@@ -115,7 +102,6 @@
     "https://bcr.bazel.build/modules/gazelle/0.51.3/MODULE.bazel": "618a729142f66de1e2cb776d026413763be5b80d5e3d29ffb9d3d90c5defde90",
     "https://bcr.bazel.build/modules/gazelle/0.51.3/source.json": "fbe5312a01fb4a2a58caff4a0d40dcf384b6f0bda75e85546663360da1d7538a",
     "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
-    "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
     "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
     "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
     "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
@@ -124,12 +110,9 @@
     "https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f",
     "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2",
     "https://bcr.bazel.build/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c",
-    "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
     "https://bcr.bazel.build/modules/jsoncpp/1.9.6.bcr.2/MODULE.bazel": "64f508885f907ac2518039b3d0c5c1703a8f6137d9f5d635067ba93d33c2670a",
     "https://bcr.bazel.build/modules/jsoncpp/1.9.6.bcr.2/source.json": "13199fa0a267ca46814e9e6ab313a71890c8f1822e57376fdc23a2d2cd384051",
-    "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0",
     "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
-    "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
     "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
     "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
     "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
@@ -146,25 +129,6 @@
     "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
     "https://bcr.bazel.build/modules/platforms/1.1.0/MODULE.bazel": "1c0c09f5bdcf4b3f924720d2478a3711cb39f4977019ca5988685e5b7e18b3d2",
     "https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344",
-    "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
-    "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a",
-    "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12",
-    "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
-    "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
-    "https://bcr.bazel.build/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295",
-    "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c",
-    "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
-    "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
-    "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
-    "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
-    "https://bcr.bazel.build/modules/protobuf/29.3/MODULE.bazel": "77480eea5fb5541903e49683f24dc3e09f4a79e0eea247414887bb9fc0066e94",
-    "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
-    "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
-    "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
-    "https://bcr.bazel.build/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e",
-    "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d",
-    "https://bcr.bazel.build/modules/protobuf/33.1/MODULE.bazel": "982c8a0cceab4d790076f72b7677faf836b0dfadc2b66a34aab7232116c4ae39",
-    "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42",
     "https://bcr.bazel.build/modules/protobuf/36.2/MODULE.bazel": "7c2b9060f7770778a606e2147e69a17465e2a08c61bfb337eaf2da61aaf62b5f",
     "https://bcr.bazel.build/modules/protobuf/36.2/source.json": "f9167721eeaa0f65b255aa30f1eb7d7655617d8d63823c3c48521159cf67672f",
     "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
@@ -182,19 +146,14 @@
     "https://bcr.bazel.build/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363",
     "https://bcr.bazel.build/modules/rules_android/0.7.3/MODULE.bazel": "1fbc49fb397d31d74be83fe63eb0a5dc1a79df172ca90772166c35ab4cd67e7b",
     "https://bcr.bazel.build/modules/rules_android/0.7.3/source.json": "80ffccd224a7f9b665f32360a64ba4228ba85c75fd0f6af43e290660147bb151",
-    "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
-    "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68",
     "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
-    "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
     "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
     "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
     "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a",
-    "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
     "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
     "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
     "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
     "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
-    "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
     "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
     "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c",
     "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
@@ -207,7 +166,6 @@
     "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
     "https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1",
     "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
-    "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
     "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
     "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
     "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
@@ -220,8 +178,6 @@
     "https://bcr.bazel.build/modules/rules_go/0.63.0/source.json": "5bf37b45c440ac03abad0cbfa8b8b8dd89cc4e82bc8305f468b8436fa58bae78",
     "https://bcr.bazel.build/modules/rules_java/9.9.0/MODULE.bazel": "6b2092957b4849ffcbb84d5c7fe0d5a4582f5e3062bc84d607122ac4ef832e55",
     "https://bcr.bazel.build/modules/rules_java/9.9.0/source.json": "9c4e694db18812d19b5c1e279ba9dffc1adc3fcac163896829f374a7c426fe12",
-    "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
-    "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
     "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
     "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d",
     "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
@@ -232,15 +188,16 @@
     "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd",
     "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
     "https://bcr.bazel.build/modules/rules_jvm_external/7.0/MODULE.bazel": "421482bdbcf05709f933c96b867a599deb517f2804ceb3e74511880610cfbf71",
-    "https://bcr.bazel.build/modules/rules_jvm_external/7.1/MODULE.bazel": "e151b1beaa45bfc9e5de4df637d980e27039eb2b1092a95972bf2adbf99ac1a0",
-    "https://bcr.bazel.build/modules/rules_jvm_external/7.1/source.json": "8cb571a9631c39bf29c1842958542f14bba1c704fe2bcde4fa54679b2a6b3d6f",
+    "https://bcr.bazel.build/modules/rules_jvm_external/7.2/MODULE.bazel": "e4f60a972780ca22128d5377d6131b4f9c000854e8bc89e212202676e41e78ce",
+    "https://bcr.bazel.build/modules/rules_jvm_external/7.2/source.json": "b0e87d55b9c5dd01852ad75641e62f16e5bcbaedd40cb43fc2a2738840e678ff",
     "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
     "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025",
     "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
     "https://bcr.bazel.build/modules/rules_kotlin/2.1.3/MODULE.bazel": "ce7def6d576aa8d3a9c6d10e13b4d157296229674371f67dbf788dae0afae3d5",
     "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/MODULE.bazel": "00d39c5e0fa78cd86193946265bb849e7878c24e44260f9525108428852b315c",
     "https://bcr.bazel.build/modules/rules_kotlin/2.3.20/MODULE.bazel": "3443d53d275e14fecfebd0b491f01d06ea3883c04a1b3336e7ae9d5ec9066bef",
-    "https://bcr.bazel.build/modules/rules_kotlin/2.3.20/source.json": "5a5553cffea43f2c5156c8ad0de4a14ad95413ceb39cd4d08f50e2aea86927e8",
+    "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/MODULE.bazel": "38dac18bb76c0a47ff60dfcd95c666985cbc46374f28ea4eeb868bdbc58c5bec",
+    "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/source.json": "07b6a307448817c071c4ba90dcb03f801e008959f8dfd8b152a241cc0ee01a23",
     "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
     "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
     "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
@@ -260,7 +217,6 @@
     "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
     "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
     "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
-    "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
     "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
     "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
     "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
@@ -269,9 +225,6 @@
     "https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500",
     "https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a",
     "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
-    "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
-    "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
-    "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6",
     "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
     "https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
     "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8",
@@ -280,8 +233,6 @@
     "https://bcr.bazel.build/modules/rules_python/2.3.4/source.json": "2111a341186d0bbe9e325a1321ad86928eab0aa6d4c3a915e8c7f53e280479ae",
     "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251",
     "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea",
-    "https://bcr.bazel.build/modules/rules_rust/0.69.0/MODULE.bazel": "4326fec48f2fef0d514de46346f7f77e200c82936dd08b91c9ef039fbdad5c10",
-    "https://bcr.bazel.build/modules/rules_rust/0.69.0/source.json": "0d094307d690cc18b3ab003998697be8070a206f65592c5c8476999796f11c4b",
     "https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc",
     "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
     "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
@@ -289,11 +240,6 @@
     "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
     "https://bcr.bazel.build/modules/rules_shell/0.8.0/MODULE.bazel": "f6a89f1d6a669a26f28fe814503857055d76306b79cfc11d12399af08d0b80ae",
     "https://bcr.bazel.build/modules/rules_shell/0.8.0/source.json": "eb53cc815bc503c6683c5fe12d943f98883f81fc22f51403ec8a95610cba4195",
-    "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
-    "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
-    "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
-    "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400",
-    "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
     "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
     "https://bcr.bazel.build/modules/stardoc/0.5.4/MODULE.bazel": "6569966df04610b8520957cb8e97cf2e9faac2c0309657c537ab51c16c18a2a4",
     "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef",
@@ -302,8 +248,6 @@
     "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
     "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
     "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
-    "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
-    "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f",
     "https://bcr.bazel.build/modules/tar.bzl/0.10.4/MODULE.bazel": "e8f9ff79199e8d9eaad7f1b0a77ad74b30bb82d794b87d8ca942bead5de83ae9",
     "https://bcr.bazel.build/modules/tar.bzl/0.10.4/source.json": "20143442376c03426f6135292ba02d825cb75308aa47e6bf42dd4cc5a435c2ff",
     "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
@@ -311,16 +255,11 @@
     "https://bcr.bazel.build/modules/toml.bzl/0.3.0/MODULE.bazel": "5016e5dd1ad2200e119a4b28b2b3935e276c4b480f2fe3e952bea7eeba88f578",
     "https://bcr.bazel.build/modules/toml.bzl/0.4.1/MODULE.bazel": "6bc0b938f03ade8d58c2fca0ad5c3fa12b4764e1e1927ad50b0c860286db2167",
     "https://bcr.bazel.build/modules/toml.bzl/0.4.1/source.json": "86a90afd8b43c9b69ad31f5c03998c3adcf4b08175e621addb93e3a38eec538b",
-    "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
-    "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9",
     "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
     "https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27",
     "https://bcr.bazel.build/modules/yq.bzl/0.3.4/source.json": "786dafdc2843722da3416e4343ee1a05237227f068590779a6e8496a2064c0f9",
-    "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
-    "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
     "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
-    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
-    "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
+    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806"
   },
   "selectedYankedVersions": {},
   "moduleExtensions": {
@@ -369,7 +308,7 @@
     },
     "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": {
       "general": {
-        "bzlTransitiveDigest": "xNW2vYQ2NGvweMnSxue40Bhfp5A3ogJIS/tkg7qjKIs=",
+        "bzlTransitiveDigest": "ZESI1GTm8zRsdU/InVvmpbo66PIBj41/6gPM33RrIns=",
         "usagesDigest": "wLrLUdBOQnztFcFQraEsWu+M4xtuF82/ZGmnhyxO5/Q=",
         "recordedInputs": [
           "REPO_MAPPING:rules_android+,bazel_tools bazel_tools"
@@ -390,7 +329,7 @@
     },
     "@@rules_android+//bzlmod_extensions:com_android_dex.bzl%com_android_dex_extension": {
       "general": {
-        "bzlTransitiveDigest": "HYWmRJkl/Bm5qA5Q7vRI52ZfSoxubYxiwbnAS+8ozAU=",
+        "bzlTransitiveDigest": "GjEt0MG4OPUpryd5nPbl/GTi4ihSBLXao0q+3Mmqv8E=",
         "usagesDigest": "QffKrRLRrfyxMU5u/o3mDLJNoWRk/D5i2SIumoSgm3M=",
         "recordedInputs": [
           "REPO_MAPPING:rules_android+,bazel_tools bazel_tools"
@@ -424,7 +363,7 @@
     },
     "@@rules_python+//python/uv:uv.bzl%uv": {
       "general": {
-        "bzlTransitiveDigest": "DafUArm1CsjJZcemT0FvUq9rUch+pq38+JqfOBJCpm8=",
+        "bzlTransitiveDigest": "+9d3Xk+XLM+3cS8xVHR6XRn3dYpPmBavXbfU8v/qtnc=",
         "usagesDigest": "V+iNLdnOiiGgW0iq4irFt01I2357trR3Bo420RwUXO0=",
         "recordedInputs": [
           "REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
@@ -452,380 +391,6 @@
         }
       }
     },
-    "@@rules_rust+//crate_universe:extension.bzl%crate": {
-      "general": {
-        "bzlTransitiveDigest": "tvVZnh8L5EmEr+F2dHIK6I1xjPQA+Ybw/b/wHKJn2qA=",
-        "usagesDigest": "x0vrCWeHpY81Qjj7vEW+k03YwVf1a+fL0Y9237dy0E4=",
-        "recordedInputs": [
-          "ENV:CARGO_BAZEL_DEBUG \\0",
-          "ENV:CARGO_BAZEL_GENERATOR_SHA256 \\0",
-          "ENV:CARGO_BAZEL_GENERATOR_URL \\0",
-          "ENV:CARGO_BAZEL_ISOLATED \\0",
-          "ENV:CARGO_BAZEL_REPIN \\0",
-          "ENV:CARGO_BAZEL_REPIN_ONLY \\0",
-          "ENV:CARGO_BAZEL_TIMEOUT \\0",
-          "ENV:REPIN \\0",
-          "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals",
-          "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version",
-          "REPO_MAPPING:rules_cc+,bazel_skylib bazel_skylib+",
-          "REPO_MAPPING:rules_cc+,bazel_tools bazel_tools",
-          "REPO_MAPPING:rules_cc+,cc_compatibility_proxy rules_cc++compatibility_proxy+cc_compatibility_proxy",
-          "REPO_MAPPING:rules_cc+,platforms platforms",
-          "REPO_MAPPING:rules_cc+,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_cc++compatibility_proxy+cc_compatibility_proxy,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_rust+,bazel_features bazel_features+",
-          "REPO_MAPPING:rules_rust+,bazel_skylib bazel_skylib+",
-          "REPO_MAPPING:rules_rust+,bazel_tools bazel_tools",
-          "REPO_MAPPING:rules_rust+,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_rust+,rules_rust rules_rust+"
-        ],
-        "generatedRepoSpecs": {
-          "crates": {
-            "repoRuleId": "@@rules_rust+//crate_universe:extensions.bzl%_generate_repo",
-            "attributes": {
-              "contents": {
-                "BUILD.bazel": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files(\n    [\n        \"cargo-bazel.json\",\n        \"crates.bzl\",\n        \"defs.bzl\",\n    ] + glob(\n        allow_empty = True,\n        include = [\"*.bazel\"],\n    ),\n)\n\nfilegroup(\n    name = \"srcs\",\n    srcs = glob(\n        allow_empty = True,\n        include = [\n            \"*.bazel\",\n            \"*.bzl\",\n        ],\n    ),\n)\n\n# Workspace Member Dependencies\nalias(\n    name = \"googletest-0.14.3\",\n    actual = \"@crates__googletest-0.14.3//:googletest\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"googletest\",\n    actual = \"@crates__googletest-0.14.3//:googletest\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"linkme-0.3.37\",\n    actual = \"@crates__linkme-0.3.37//:linkme\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"linkme\",\n    actual = \"@crates__linkme-0.3.37//:linkme\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"paste-1.0.15\",\n    actual = \"@crates__paste-1.0.15//:paste\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"paste\",\n    actual = \"@crates__paste-1.0.15//:paste\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"quote-1.0.47\",\n    actual = \"@crates__quote-1.0.47//:quote\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"quote\",\n    actual = \"@crates__quote-1.0.47//:quote\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"syn-2.0.119\",\n    actual = \"@crates__syn-2.0.119//:syn\",\n    tags = [\"manual\"],\n)\n\nalias(\n    name = \"syn\",\n    actual = \"@crates__syn-2.0.119//:syn\",\n    tags = [\"manual\"],\n)\n",
-                "alias_rules.bzl": "\"\"\"Alias that transitions its target to `compilation_mode=opt`.  Use `transition_alias=\"opt\"` to enable.\"\"\"\n\nload(\"@rules_cc//cc:defs.bzl\", \"CcInfo\")\nload(\"@rules_rust//rust:rust_common.bzl\", \"COMMON_PROVIDERS\")\n\ndef _transition_alias_impl(ctx):\n    # `ctx.attr.actual` is a list of 1 item due to the transition\n    providers = [ctx.attr.actual[0][provider] for provider in COMMON_PROVIDERS]\n    if CcInfo in ctx.attr.actual[0]:\n        providers.append(ctx.attr.actual[0][CcInfo])\n    return providers\n\ndef _change_compilation_mode(compilation_mode):\n    def _change_compilation_mode_impl(_settings, _attr):\n        return {\n            \"//command_line_option:compilation_mode\": compilation_mode,\n        }\n\n    return transition(\n        implementation = _change_compilation_mode_impl,\n        inputs = [],\n        outputs = [\n            \"//command_line_option:compilation_mode\",\n        ],\n    )\n\ndef _transition_alias_rule(compilation_mode):\n    return rule(\n        implementation = _transition_alias_impl,\n        provides = COMMON_PROVIDERS,\n        attrs = {\n            \"actual\": attr.label(\n                mandatory = True,\n                doc = \"`rust_library()` target to transition to `compilation_mode=opt`.\",\n                providers = COMMON_PROVIDERS,\n                cfg = _change_compilation_mode(compilation_mode),\n            ),\n            \"_allowlist_function_transition\": attr.label(\n                default = \"@bazel_tools//tools/allowlists/function_transition_allowlist\",\n            ),\n        },\n        doc = \"Transitions a Rust library crate to the `compilation_mode=opt`.\",\n    )\n\ntransition_alias_dbg = _transition_alias_rule(\"dbg\")\ntransition_alias_fastbuild = _transition_alias_rule(\"fastbuild\")\ntransition_alias_opt = _transition_alias_rule(\"opt\")\n",
-                "defs.bzl": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\"\"\"\n# `crates_repository` API\n\n- [aliases](#aliases)\n- [crate_deps](#crate_deps)\n- [all_crate_deps](#all_crate_deps)\n- [crate_repositories](#crate_repositories)\n\n\"\"\"\n\nload(\"@bazel_tools//tools/build_defs/repo:git.bzl\", \"new_git_repository\")\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\nload(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")\nload(\"@bazel_skylib//lib:selects.bzl\", \"selects\")\nload(\"@rules_rust//crate_universe/private:local_crate_mirror.bzl\", \"local_crate_mirror\")\n\n###############################################################################\n# MACROS API\n###############################################################################\n\n# An identifier that represent common dependencies (unconditional).\n_COMMON_CONDITION = \"\"\n\ndef _flatten_dependency_maps(all_dependency_maps):\n    \"\"\"Flatten a list of dependency maps into one dictionary.\n\n    Dependency maps have the following structure:\n\n    ```python\n    DEPENDENCIES_MAP = {\n        # The first key in the map is a Bazel package\n        # name of the workspace this file is defined in.\n        \"workspace_member_package\": {\n\n            # Not all dependencies are supported for all platforms.\n            # the condition key is the condition required to be true\n            # on the host platform.\n            \"condition\": {\n\n                # An alias to a crate target.     # The label of the crate target the\n                # Aliases are only crate names.   # package name refers to.\n                \"package_name\":                   \"@full//:label\",\n            }\n        }\n    }\n    ```\n\n    Args:\n        all_dependency_maps (list): A list of dicts as described above\n\n    Returns:\n        dict: A dictionary as described above\n    \"\"\"\n    dependencies = {}\n\n    for workspace_deps_map in all_dependency_maps:\n        for pkg_name, conditional_deps_map in workspace_deps_map.items():\n            if pkg_name not in dependencies:\n                non_frozen_map = dict()\n                for key, values in conditional_deps_map.items():\n                    non_frozen_map.update({key: dict(values.items())})\n                dependencies.setdefault(pkg_name, non_frozen_map)\n                continue\n\n            for condition, deps_map in conditional_deps_map.items():\n                # If the condition has not been recorded, do so and continue\n                if condition not in dependencies[pkg_name]:\n                    dependencies[pkg_name].setdefault(condition, dict(deps_map.items()))\n                    continue\n\n                # Alert on any miss-matched dependencies\n                inconsistent_entries = []\n                for crate_name, crate_label in deps_map.items():\n                    existing = dependencies[pkg_name][condition].get(crate_name)\n                    if existing and existing != crate_label:\n                        inconsistent_entries.append((crate_name, existing, crate_label))\n                    dependencies[pkg_name][condition].update({crate_name: crate_label})\n\n    return dependencies\n\ndef crate_deps(deps, package_name = None):\n    \"\"\"Finds the fully qualified label of the requested crates for the package where this macro is called.\n\n    Args:\n        deps (list): The desired list of crate targets.\n        package_name (str, optional): The package name of the set of dependencies to look up.\n            Defaults to `native.package_name()`.\n\n    Returns:\n        list: A list of labels to generated rust targets (str)\n    \"\"\"\n\n    if not deps:\n        return []\n\n    if package_name == None:\n        package_name = native.package_name()\n\n    # Join both sets of dependencies\n    dependencies = _flatten_dependency_maps([\n        _NORMAL_DEPENDENCIES,\n        _NORMAL_DEV_DEPENDENCIES,\n        _PROC_MACRO_DEPENDENCIES,\n        _PROC_MACRO_DEV_DEPENDENCIES,\n        _BUILD_DEPENDENCIES,\n        _BUILD_PROC_MACRO_DEPENDENCIES,\n    ]).pop(package_name, {})\n\n    # Combine all conditional packages so we can easily index over a flat list\n    # TODO: Perhaps this should actually return select statements and maintain\n    # the conditionals of the dependencies\n    flat_deps = {}\n    for deps_set in dependencies.values():\n        for crate_name, crate_label in deps_set.items():\n            flat_deps.update({crate_name: crate_label})\n\n    missing_crates = []\n    crate_targets = []\n    for crate_target in deps:\n        if crate_target not in flat_deps:\n            missing_crates.append(crate_target)\n        else:\n            crate_targets.append(flat_deps[crate_target])\n\n    if missing_crates:\n        fail(\"Could not find crates `{}` among dependencies of `{}`. Available dependencies were `{}`\".format(\n            missing_crates,\n            package_name,\n            dependencies,\n        ))\n\n    return crate_targets\n\ndef all_crate_deps(\n        normal = False, \n        normal_dev = False, \n        proc_macro = False, \n        proc_macro_dev = False,\n        build = False,\n        build_proc_macro = False,\n        package_name = None):\n    \"\"\"Finds the fully qualified label of all requested direct crate dependencies \\\n    for the package where this macro is called.\n\n    If no parameters are set, all normal dependencies are returned. Setting any one flag will\n    otherwise impact the contents of the returned list.\n\n    Args:\n        normal (bool, optional): If True, normal dependencies are included in the\n            output list.\n        normal_dev (bool, optional): If True, normal dev dependencies will be\n            included in the output list.\n        proc_macro (bool, optional): If True, proc_macro dependencies are included\n            in the output list.\n        proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n            included in the output list.\n        build (bool, optional): If True, build dependencies are included\n            in the output list.\n        build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n            included in the output list.\n        package_name (str, optional): The package name of the set of dependencies to look up.\n            Defaults to `native.package_name()` when unset.\n\n    Returns:\n        list: A list of labels to generated rust targets (str)\n    \"\"\"\n\n    if package_name == None:\n        package_name = native.package_name()\n\n    # Determine the relevant maps to use\n    all_dependency_maps = []\n    if normal:\n        all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n    if normal_dev:\n        all_dependency_maps.append(_NORMAL_DEV_DEPENDENCIES)\n    if proc_macro:\n        all_dependency_maps.append(_PROC_MACRO_DEPENDENCIES)\n    if proc_macro_dev:\n        all_dependency_maps.append(_PROC_MACRO_DEV_DEPENDENCIES)\n    if build:\n        all_dependency_maps.append(_BUILD_DEPENDENCIES)\n    if build_proc_macro:\n        all_dependency_maps.append(_BUILD_PROC_MACRO_DEPENDENCIES)\n\n    # Default to always using normal dependencies\n    if not all_dependency_maps:\n        all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n\n    dependencies = _flatten_dependency_maps(all_dependency_maps).pop(package_name, None)\n\n    if not dependencies:\n        if dependencies == None:\n            fail(\"Tried to get all_crate_deps for package \" + package_name + \" but that package had no Cargo.toml file\")\n        else:\n            return []\n\n    crate_deps = list(dependencies.pop(_COMMON_CONDITION, {}).values())\n    for condition, deps in dependencies.items():\n        crate_deps += selects.with_or({\n            tuple(_CONDITIONS[condition]): deps.values(),\n            \"//conditions:default\": [],\n        })\n\n    return crate_deps\n\ndef aliases(\n        normal = False,\n        normal_dev = False,\n        proc_macro = False,\n        proc_macro_dev = False,\n        build = False,\n        build_proc_macro = False,\n        package_name = None):\n    \"\"\"Produces a map of Crate alias names to their original label\n\n    If no dependency kinds are specified, `normal` and `proc_macro` are used by default.\n    Setting any one flag will otherwise determine the contents of the returned dict.\n\n    Args:\n        normal (bool, optional): If True, normal dependencies are included in the\n            output list.\n        normal_dev (bool, optional): If True, normal dev dependencies will be\n            included in the output list..\n        proc_macro (bool, optional): If True, proc_macro dependencies are included\n            in the output list.\n        proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n            included in the output list.\n        build (bool, optional): If True, build dependencies are included\n            in the output list.\n        build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n            included in the output list.\n        package_name (str, optional): The package name of the set of dependencies to look up.\n            Defaults to `native.package_name()` when unset.\n\n    Returns:\n        dict: The aliases of all associated packages\n    \"\"\"\n    if package_name == None:\n        package_name = native.package_name()\n\n    # Determine the relevant maps to use\n    all_aliases_maps = []\n    if normal:\n        all_aliases_maps.append(_NORMAL_ALIASES)\n    if normal_dev:\n        all_aliases_maps.append(_NORMAL_DEV_ALIASES)\n    if proc_macro:\n        all_aliases_maps.append(_PROC_MACRO_ALIASES)\n    if proc_macro_dev:\n        all_aliases_maps.append(_PROC_MACRO_DEV_ALIASES)\n    if build:\n        all_aliases_maps.append(_BUILD_ALIASES)\n    if build_proc_macro:\n        all_aliases_maps.append(_BUILD_PROC_MACRO_ALIASES)\n\n    # Default to always using normal aliases\n    if not all_aliases_maps:\n        all_aliases_maps.append(_NORMAL_ALIASES)\n        all_aliases_maps.append(_PROC_MACRO_ALIASES)\n\n    aliases = _flatten_dependency_maps(all_aliases_maps).pop(package_name, None)\n\n    if not aliases:\n        return dict()\n\n    common_items = aliases.pop(_COMMON_CONDITION, {}).items()\n\n    # If there are only common items in the dictionary, immediately return them\n    if not len(aliases.keys()) == 1:\n        return dict(common_items)\n\n    # Build a single select statement where each conditional has accounted for the\n    # common set of aliases.\n    crate_aliases = {\"//conditions:default\": dict(common_items)}\n    for condition, deps in aliases.items():\n        condition_triples = _CONDITIONS[condition]\n        for triple in condition_triples:\n            if triple in crate_aliases:\n                crate_aliases[triple].update(deps)\n            else:\n                crate_aliases.update({triple: dict(deps.items() + common_items)})\n\n    return select(crate_aliases)\n\n###############################################################################\n# WORKSPACE MEMBER DEPS AND ALIASES\n###############################################################################\n\n_NORMAL_DEPENDENCIES = {\n    \"\": {\n        _COMMON_CONDITION: {\n            \"googletest\": Label(\"@crates//:googletest-0.14.3\"),\n            \"linkme\": Label(\"@crates//:linkme-0.3.37\"),\n            \"quote\": Label(\"@crates//:quote-1.0.47\"),\n            \"syn\": Label(\"@crates//:syn-2.0.119\"),\n        },\n    },\n}\n\n\n_NORMAL_ALIASES = {\n    \"\": {\n        _COMMON_CONDITION: {\n        },\n    },\n}\n\n\n_NORMAL_DEV_DEPENDENCIES = {\n    \"\": {\n    },\n}\n\n\n_NORMAL_DEV_ALIASES = {\n    \"\": {\n    },\n}\n\n\n_PROC_MACRO_DEPENDENCIES = {\n    \"\": {\n        _COMMON_CONDITION: {\n            \"paste\": Label(\"@crates//:paste-1.0.15\"),\n        },\n    },\n}\n\n\n_PROC_MACRO_ALIASES = {\n    \"\": {\n    },\n}\n\n\n_PROC_MACRO_DEV_DEPENDENCIES = {\n    \"\": {\n    },\n}\n\n\n_PROC_MACRO_DEV_ALIASES = {\n    \"\": {\n    },\n}\n\n\n_BUILD_DEPENDENCIES = {\n    \"\": {\n    },\n}\n\n\n_BUILD_ALIASES = {\n    \"\": {\n    },\n}\n\n\n_BUILD_PROC_MACRO_DEPENDENCIES = {\n    \"\": {\n    },\n}\n\n\n_BUILD_PROC_MACRO_ALIASES = {\n    \"\": {\n    },\n}\n\n\n_CONDITIONS = {\n    \"aarch64-apple-darwin\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n    \"aarch64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n    \"wasm32-unknown-unknown\": [\"@rules_rust//rust/platform:wasm32-unknown-unknown\"],\n    \"wasm32-wasip1\": [\"@rules_rust//rust/platform:wasm32-wasip1\"],\n    \"x86_64-pc-windows-msvc\": [\"@rules_rust//rust/platform:x86_64-pc-windows-msvc\"],\n    \"x86_64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n    \"x86_64-unknown-nixos-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\"],\n}\n\n###############################################################################\n\ndef crate_repositories():\n    \"\"\"A macro for defining repositories for all generated crates.\n\n    Returns:\n      A list of repos visible to the module through the module extension.\n    \"\"\"\n    maybe(\n        http_archive,\n        name = \"crates__aho-corasick-1.1.5\",\n        sha256 = \"c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/aho-corasick/1.1.5/download\"],\n        strip_prefix = \"aho-corasick-1.1.5\",\n        build_file = Label(\"@crates//crates:BUILD.aho-corasick-1.1.5.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__autocfg-1.5.1\",\n        sha256 = \"f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/autocfg/1.5.1/download\"],\n        strip_prefix = \"autocfg-1.5.1\",\n        build_file = Label(\"@crates//crates:BUILD.autocfg-1.5.1.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__googletest-0.14.3\",\n        sha256 = \"f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/googletest/0.14.3/download\"],\n        strip_prefix = \"googletest-0.14.3\",\n        build_file = Label(\"@crates//crates:BUILD.googletest-0.14.3.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__googletest_macro-0.14.3\",\n        sha256 = \"2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/googletest_macro/0.14.3/download\"],\n        strip_prefix = \"googletest_macro-0.14.3\",\n        build_file = Label(\"@crates//crates:BUILD.googletest_macro-0.14.3.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__linkme-0.3.37\",\n        sha256 = \"3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/linkme/0.3.37/download\"],\n        strip_prefix = \"linkme-0.3.37\",\n        build_file = Label(\"@crates//crates:BUILD.linkme-0.3.37.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__linkme-impl-0.3.37\",\n        sha256 = \"77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/linkme-impl/0.3.37/download\"],\n        strip_prefix = \"linkme-impl-0.3.37\",\n        build_file = Label(\"@crates//crates:BUILD.linkme-impl-0.3.37.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__memchr-2.8.3\",\n        sha256 = \"cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/memchr/2.8.3/download\"],\n        strip_prefix = \"memchr-2.8.3\",\n        build_file = Label(\"@crates//crates:BUILD.memchr-2.8.3.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__num-traits-0.2.19\",\n        sha256 = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/num-traits/0.2.19/download\"],\n        strip_prefix = \"num-traits-0.2.19\",\n        build_file = Label(\"@crates//crates:BUILD.num-traits-0.2.19.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__paste-1.0.15\",\n        sha256 = \"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/paste/1.0.15/download\"],\n        strip_prefix = \"paste-1.0.15\",\n        build_file = Label(\"@crates//crates:BUILD.paste-1.0.15.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__proc-macro2-1.0.107\",\n        sha256 = \"985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/proc-macro2/1.0.107/download\"],\n        strip_prefix = \"proc-macro2-1.0.107\",\n        build_file = Label(\"@crates//crates:BUILD.proc-macro2-1.0.107.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__quote-1.0.47\",\n        sha256 = \"1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/quote/1.0.47/download\"],\n        strip_prefix = \"quote-1.0.47\",\n        build_file = Label(\"@crates//crates:BUILD.quote-1.0.47.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__regex-1.13.1\",\n        sha256 = \"f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/regex/1.13.1/download\"],\n        strip_prefix = \"regex-1.13.1\",\n        build_file = Label(\"@crates//crates:BUILD.regex-1.13.1.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__regex-automata-0.4.18\",\n        sha256 = \"ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/regex-automata/0.4.18/download\"],\n        strip_prefix = \"regex-automata-0.4.18\",\n        build_file = Label(\"@crates//crates:BUILD.regex-automata-0.4.18.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__regex-syntax-0.8.11\",\n        sha256 = \"d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/regex-syntax/0.8.11/download\"],\n        strip_prefix = \"regex-syntax-0.8.11\",\n        build_file = Label(\"@crates//crates:BUILD.regex-syntax-0.8.11.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__rustversion-1.0.23\",\n        sha256 = \"cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/rustversion/1.0.23/download\"],\n        strip_prefix = \"rustversion-1.0.23\",\n        build_file = Label(\"@crates//crates:BUILD.rustversion-1.0.23.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__syn-2.0.119\",\n        sha256 = \"872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/syn/2.0.119/download\"],\n        strip_prefix = \"syn-2.0.119\",\n        build_file = Label(\"@crates//crates:BUILD.syn-2.0.119.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__syn-3.0.6\",\n        sha256 = \"8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/syn/3.0.6/download\"],\n        strip_prefix = \"syn-3.0.6\",\n        build_file = Label(\"@crates//crates:BUILD.syn-3.0.6.bazel\"),\n    )\n\n    maybe(\n        http_archive,\n        name = \"crates__unicode-ident-1.0.26\",\n        sha256 = \"d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954\",\n        type = \"tar.gz\",\n        urls = [\"https://static.crates.io/crates/unicode-ident/1.0.26/download\"],\n        strip_prefix = \"unicode-ident-1.0.26\",\n        build_file = Label(\"@crates//crates:BUILD.unicode-ident-1.0.26.bazel\"),\n    )\n\n    return [\n       struct(repo=\"crates__googletest-0.14.3\", is_dev_dep = False),\n       struct(repo=\"crates__linkme-0.3.37\", is_dev_dep = False),\n       struct(repo=\"crates__paste-1.0.15\", is_dev_dep = False),\n       struct(repo=\"crates__quote-1.0.47\", is_dev_dep = False),\n       struct(repo=\"crates__syn-2.0.119\", is_dev_dep = False),\n    ]\n"
-              }
-            }
-          },
-          "crates__aho-corasick-1.1.5": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/aho-corasick/1.1.5/download"
-              ],
-              "strip_prefix": "aho-corasick-1.1.5",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"aho_corasick\",\n    deps = [\n        \"@crates__memchr-2.8.3//:memchr\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"perf-literal\",\n        \"std\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=aho-corasick\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.1.5\",\n)\n"
-            }
-          },
-          "crates__autocfg-1.5.1": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/autocfg/1.5.1/download"
-              ],
-              "strip_prefix": "autocfg-1.5.1",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"autocfg\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2015\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=autocfg\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.5.1\",\n)\n"
-            }
-          },
-          "crates__googletest-0.14.3": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/googletest/0.14.3/download"
-              ],
-              "strip_prefix": "googletest-0.14.3",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"googletest\",\n    deps = [\n        \"@crates__num-traits-0.2.19//:num_traits\",\n        \"@crates__regex-1.13.1//:regex\",\n    ],\n    proc_macro_deps = [\n        \"@crates__googletest_macro-0.14.3//:googletest_macro\",\n        \"@crates__rustversion-1.0.23//:rustversion\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=googletest\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.14.3\",\n)\n"
-            }
-          },
-          "crates__googletest_macro-0.14.3": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/googletest_macro/0.14.3/download"
-              ],
-              "strip_prefix": "googletest_macro-0.14.3",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n    name = \"googletest_macro\",\n    deps = [\n        \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n        \"@crates__quote-1.0.47//:quote\",\n        \"@crates__syn-2.0.119//:syn\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=googletest_macro\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.14.3\",\n)\n"
-            }
-          },
-          "crates__linkme-0.3.37": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/linkme/0.3.37/download"
-              ],
-              "strip_prefix": "linkme-0.3.37",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"linkme\",\n    deps = [\n        \"@crates__linkme-0.3.37//:build_script_build\",\n    ],\n    proc_macro_deps = [\n        \"@crates__linkme-impl-0.3.37//:linkme_impl\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=linkme\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.3.37\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2021\",\n    pkg_name = \"linkme\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=linkme\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"0.3.37\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__linkme-impl-0.3.37": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/linkme-impl/0.3.37/download"
-              ],
-              "strip_prefix": "linkme-impl-0.3.37",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n    name = \"linkme_impl\",\n    deps = [\n        \"@crates__linkme-impl-0.3.37//:build_script_build\",\n        \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n        \"@crates__quote-1.0.47//:quote\",\n        \"@crates__syn-3.0.6//:syn\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=linkme-impl\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.3.37\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2021\",\n    pkg_name = \"linkme-impl\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=linkme-impl\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"0.3.37\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__memchr-2.8.3": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/memchr/2.8.3/download"
-              ],
-              "strip_prefix": "memchr-2.8.3",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"memchr\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"alloc\",\n        \"std\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=memchr\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"2.8.3\",\n)\n"
-            }
-          },
-          "crates__num-traits-0.2.19": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/num-traits/0.2.19/download"
-              ],
-              "strip_prefix": "num-traits-0.2.19",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"num_traits\",\n    deps = [\n        \"@crates__num-traits-0.2.19//:build_script_build\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"std\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=num-traits\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.2.19\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"std\",\n    ],\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    deps = [\n        \"@crates__autocfg-1.5.1//:autocfg\",\n    ],\n    edition = \"2021\",\n    pkg_name = \"num-traits\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=num-traits\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"0.2.19\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__paste-1.0.15": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/paste/1.0.15/download"
-              ],
-              "strip_prefix": "paste-1.0.15",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n    name = \"paste\",\n    deps = [\n        \"@crates__paste-1.0.15//:build_script_build\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2018\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=paste\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.0.15\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2018\",\n    pkg_name = \"paste\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=paste\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"1.0.15\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__proc-macro2-1.0.107": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/proc-macro2/1.0.107/download"
-              ],
-              "strip_prefix": "proc-macro2-1.0.107",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"proc_macro2\",\n    deps = [\n        \"@crates__proc-macro2-1.0.107//:build_script_build\",\n        \"@crates__unicode-ident-1.0.26//:unicode_ident\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"proc-macro\",\n    ] + select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n            \"default\",  # aarch64-apple-darwin\n        ],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n            \"default\",  # aarch64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n            \"default\",  # x86_64-pc-windows-msvc\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n            \"default\",  # x86_64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n            \"default\",  # x86_64-unknown-nixos-gnu\n        ],\n        \"//conditions:default\": [],\n    }),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=proc-macro2\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.0.107\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"proc-macro\",\n    ] + select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n            \"default\",  # aarch64-apple-darwin\n        ],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n            \"default\",  # aarch64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n            \"default\",  # x86_64-pc-windows-msvc\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n            \"default\",  # x86_64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n            \"default\",  # x86_64-unknown-nixos-gnu\n        ],\n        \"//conditions:default\": [],\n    }),\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2021\",\n    pkg_name = \"proc-macro2\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=proc-macro2\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"1.0.107\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__quote-1.0.47": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/quote/1.0.47/download"
-              ],
-              "strip_prefix": "quote-1.0.47",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"quote\",\n    deps = [\n        \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n        \"@crates__quote-1.0.47//:build_script_build\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"proc-macro\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=quote\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.0.47\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"proc-macro\",\n    ],\n    crate_name = \"build_script_build\",\n    crate_root = \"build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2021\",\n    pkg_name = \"quote\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=quote\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"1.0.47\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__regex-1.13.1": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/regex/1.13.1/download"
-              ],
-              "strip_prefix": "regex-1.13.1",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"regex\",\n    deps = [\n        \"@crates__aho-corasick-1.1.5//:aho_corasick\",\n        \"@crates__memchr-2.8.3//:memchr\",\n        \"@crates__regex-automata-0.4.18//:regex_automata\",\n        \"@crates__regex-syntax-0.8.11//:regex_syntax\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"perf\",\n        \"perf-backtrack\",\n        \"perf-cache\",\n        \"perf-dfa\",\n        \"perf-inline\",\n        \"perf-literal\",\n        \"perf-onepass\",\n        \"std\",\n        \"unicode\",\n        \"unicode-age\",\n        \"unicode-bool\",\n        \"unicode-case\",\n        \"unicode-gencat\",\n        \"unicode-perl\",\n        \"unicode-script\",\n        \"unicode-segment\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=regex\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.13.1\",\n)\n"
-            }
-          },
-          "crates__regex-automata-0.4.18": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/regex-automata/0.4.18/download"
-              ],
-              "strip_prefix": "regex-automata-0.4.18",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"regex_automata\",\n    deps = [\n        \"@crates__aho-corasick-1.1.5//:aho_corasick\",\n        \"@crates__memchr-2.8.3//:memchr\",\n        \"@crates__regex-syntax-0.8.11//:regex_syntax\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"alloc\",\n        \"dfa-onepass\",\n        \"hybrid\",\n        \"meta\",\n        \"nfa-backtrack\",\n        \"nfa-pikevm\",\n        \"nfa-thompson\",\n        \"perf-inline\",\n        \"perf-literal\",\n        \"perf-literal-multisubstring\",\n        \"perf-literal-substring\",\n        \"std\",\n        \"syntax\",\n        \"unicode\",\n        \"unicode-age\",\n        \"unicode-bool\",\n        \"unicode-case\",\n        \"unicode-gencat\",\n        \"unicode-perl\",\n        \"unicode-script\",\n        \"unicode-segment\",\n        \"unicode-word-boundary\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=regex-automata\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.4.18\",\n)\n"
-            }
-          },
-          "crates__regex-syntax-0.8.11": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/regex-syntax/0.8.11/download"
-              ],
-              "strip_prefix": "regex-syntax-0.8.11",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"regex_syntax\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"default\",\n        \"std\",\n        \"unicode\",\n        \"unicode-age\",\n        \"unicode-bool\",\n        \"unicode-case\",\n        \"unicode-gencat\",\n        \"unicode-perl\",\n        \"unicode-script\",\n        \"unicode-segment\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=regex-syntax\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"0.8.11\",\n)\n"
-            }
-          },
-          "crates__rustversion-1.0.23": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/rustversion/1.0.23/download"
-              ],
-              "strip_prefix": "rustversion-1.0.23",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n    \"@rules_rust//cargo:defs.bzl\",\n    \"cargo_build_script\",\n    \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n    name = \"rustversion\",\n    deps = [\n        \"@crates__rustversion-1.0.23//:build_script_build\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2018\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=rustversion\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.0.23\",\n)\n\ncargo_build_script(\n    name = \"_bs\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \"**/*.rs\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_name = \"build_script_build\",\n    crate_root = \"build/build.rs\",\n    data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    edition = \"2018\",\n    pkg_name = \"rustversion\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=rustversion\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    version = \"1.0.23\",\n    visibility = [\"//visibility:private\"],\n)\n\nalias(\n    name = \"build_script_build\",\n    actual = \":_bs\",\n    tags = [\"manual\"],\n)\n"
-            }
-          },
-          "crates__syn-2.0.119": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/syn/2.0.119/download"
-              ],
-              "strip_prefix": "syn-2.0.119",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"syn\",\n    deps = [\n        \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n        \"@crates__quote-1.0.47//:quote\",\n        \"@crates__unicode-ident-1.0.26//:unicode_ident\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"clone-impls\",\n        \"default\",\n        \"derive\",\n        \"parsing\",\n        \"printing\",\n        \"proc-macro\",\n    ] + select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n            \"extra-traits\",  # aarch64-apple-darwin\n            \"full\",  # aarch64-apple-darwin\n        ],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n            \"extra-traits\",  # aarch64-unknown-linux-gnu\n            \"full\",  # aarch64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n            \"extra-traits\",  # x86_64-pc-windows-msvc\n            \"full\",  # x86_64-pc-windows-msvc\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n            \"extra-traits\",  # x86_64-unknown-linux-gnu\n            \"full\",  # x86_64-unknown-linux-gnu\n        ],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n            \"extra-traits\",  # x86_64-unknown-nixos-gnu\n            \"full\",  # x86_64-unknown-nixos-gnu\n        ],\n        \"//conditions:default\": [],\n    }),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=syn\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"2.0.119\",\n)\n"
-            }
-          },
-          "crates__syn-3.0.6": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/syn/3.0.6/download"
-              ],
-              "strip_prefix": "syn-3.0.6",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"syn\",\n    deps = [\n        \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n        \"@crates__quote-1.0.47//:quote\",\n        \"@crates__unicode-ident-1.0.26//:unicode_ident\",\n    ],\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_features = [\n        \"clone-impls\",\n        \"default\",\n        \"derive\",\n        \"parsing\",\n        \"printing\",\n        \"proc-macro\",\n    ],\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=syn\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"3.0.6\",\n)\n"
-            }
-          },
-          "crates__unicode-ident-1.0.26": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "remote_patch_strip": 1,
-              "sha256": "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954",
-              "type": "tar.gz",
-              "urls": [
-                "https://static.crates.io/crates/unicode-ident/1.0.26/download"
-              ],
-              "strip_prefix": "unicode-ident-1.0.26",
-              "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n#     bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n    name = \"cargo_toml_env_vars\",\n    src = \"Cargo.toml\",\n)\n\nrust_library(\n    name = \"unicode_ident\",\n    compile_data = glob(\n        allow_empty = True,\n        include = [\"**\"],\n        exclude = [\n            \"**/* *\",\n            \".tmp_git_root/**/*\",\n            \"BUILD\",\n            \"BUILD.bazel\",\n            \"WORKSPACE\",\n            \"WORKSPACE.bazel\",\n        ],\n    ),\n    crate_root = \"src/lib.rs\",\n    edition = \"2021\",\n    rustc_env_files = [\n        \":cargo_toml_env_vars\",\n    ],\n    rustc_flags = [\n        \"--cap-lints=allow\",\n    ],\n    srcs = glob(\n        allow_empty = True,\n        include = [\"**/*.rs\"],\n    ),\n    tags = [\n        \"cargo-bazel\",\n        \"crate-name=unicode-ident\",\n        \"manual\",\n        \"noclippy\",\n        \"norustfmt\",\n    ],\n    target_compatible_with = select({\n        \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n        \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n        \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n        \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n        \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n        \"//conditions:default\": [\"@platforms//:incompatible\"],\n    }),\n    version = \"1.0.26\",\n)\n"
-            }
-          }
-        }
-      }
-    },
-    "@@rules_rust+//crate_universe/private:internal_extensions.bzl%cu_nr": {
-      "general": {
-        "bzlTransitiveDigest": "1HzZEEx7jfZ/0qVRcLy/gGqX5dP/CDJjvkQx8gq6E8E=",
-        "usagesDigest": "tG3p3Nb5XxC7vWY/bcKdb//g0HoAxpxxH3F5/jBVlk4=",
-        "recordedInputs": [
-          "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals",
-          "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version",
-          "REPO_MAPPING:rules_cc+,bazel_skylib bazel_skylib+",
-          "REPO_MAPPING:rules_cc+,bazel_tools bazel_tools",
-          "REPO_MAPPING:rules_cc+,cc_compatibility_proxy rules_cc++compatibility_proxy+cc_compatibility_proxy",
-          "REPO_MAPPING:rules_cc+,platforms platforms",
-          "REPO_MAPPING:rules_cc+,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_cc++compatibility_proxy+cc_compatibility_proxy,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_rust+,bazel_features bazel_features+",
-          "REPO_MAPPING:rules_rust+,bazel_skylib bazel_skylib+",
-          "REPO_MAPPING:rules_rust+,bazel_tools bazel_tools",
-          "REPO_MAPPING:rules_rust+,cui rules_rust++cu+cui",
-          "REPO_MAPPING:rules_rust+,rrc rules_rust++i2+rrc",
-          "REPO_MAPPING:rules_rust+,rules_cc rules_cc+",
-          "REPO_MAPPING:rules_rust+,rules_rust rules_rust+"
-        ],
-        "generatedRepoSpecs": {
-          "cargo_bazel_bootstrap": {
-            "repoRuleId": "@@rules_rust+//cargo/private:cargo_bootstrap.bzl%cargo_bootstrap_repository",
-            "attributes": {
-              "srcs": [
-                "@@rules_rust+//crate_universe:src/api.rs",
-                "@@rules_rust+//crate_universe:src/api/lockfile.rs",
-                "@@rules_rust+//crate_universe:src/cli.rs",
-                "@@rules_rust+//crate_universe:src/cli/generate.rs",
-                "@@rules_rust+//crate_universe:src/cli/query.rs",
-                "@@rules_rust+//crate_universe:src/cli/render.rs",
-                "@@rules_rust+//crate_universe:src/cli/splice.rs",
-                "@@rules_rust+//crate_universe:src/cli/vendor.rs",
-                "@@rules_rust+//crate_universe:src/config.rs",
-                "@@rules_rust+//crate_universe:src/context.rs",
-                "@@rules_rust+//crate_universe:src/context/crate_context.rs",
-                "@@rules_rust+//crate_universe:src/context/platforms.rs",
-                "@@rules_rust+//crate_universe:src/lib.rs",
-                "@@rules_rust+//crate_universe:src/lockfile.rs",
-                "@@rules_rust+//crate_universe:src/main.rs",
-                "@@rules_rust+//crate_universe:src/metadata.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_bin.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_resolver.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.bat",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.sh",
-                "@@rules_rust+//crate_universe:src/metadata/dependency.rs",
-                "@@rules_rust+//crate_universe:src/metadata/metadata_annotation.rs",
-                "@@rules_rust+//crate_universe:src/rendering.rs",
-                "@@rules_rust+//crate_universe:src/rendering/template_engine.rs",
-                "@@rules_rust+//crate_universe:src/rendering/templates/module_bzl.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/header.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/aliases_map.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/deps_map.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_git.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_http.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/vendor_module.j2",
-                "@@rules_rust+//crate_universe:src/rendering/verbatim/alias_rules.bzl",
-                "@@rules_rust+//crate_universe:src/select.rs",
-                "@@rules_rust+//crate_universe:src/splicing.rs",
-                "@@rules_rust+//crate_universe:src/splicing/cargo_config.rs",
-                "@@rules_rust+//crate_universe:src/splicing/crate_index_lookup.rs",
-                "@@rules_rust+//crate_universe:src/splicing/splicer.rs",
-                "@@rules_rust+//crate_universe:src/test.rs",
-                "@@rules_rust+//crate_universe:src/utils.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/glob.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/label.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_dict.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_list.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_scalar.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_set.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/serialize.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/target_compatible_with.rs",
-                "@@rules_rust+//crate_universe:src/utils/symlink.rs",
-                "@@rules_rust+//crate_universe:src/utils/target_triple.rs"
-              ],
-              "binary": "cargo-bazel",
-              "cargo_lockfile": "@@rules_rust+//crate_universe:Cargo.lock",
-              "cargo_toml": "@@rules_rust+//crate_universe:Cargo.toml",
-              "version": "1.93.1",
-              "timeout": 900,
-              "rust_toolchain_cargo_template": "@rust_host_tools//:bin/{tool}",
-              "rust_toolchain_rustc_template": "@rust_host_tools//:bin/{tool}",
-              "compressed_windows_toolchain_names": false
-            }
-          }
-        },
-        "moduleExtensionMetadata": {
-          "explicitRootModuleDirectDeps": [
-            "cargo_bazel_bootstrap"
-          ],
-          "explicitRootModuleDirectDevDeps": [],
-          "useAllRepos": "NO",
-          "reproducible": false
-        }
-      }
-    },
     "@@yq.bzl+//yq:extensions.bzl%yq": {
       "general": {
         "bzlTransitiveDigest": "tDqk+ntWTdxNAWPDjRY1uITgHbti2jcXR5ZdinltBs0=",
diff --git a/configs/agent_configs.textproto b/configs/agent_configs.textproto
index c16bf16b..612d7c5 100644
--- a/configs/agent_configs.textproto
+++ b/configs/agent_configs.textproto
@@ -62,3 +62,15 @@
   automatic: true
 }
 
+# Default Multi-Perspective Code Review Agent
+configs: {
+  id: "code-review-agent"
+  display_name: "Code Review Agent"
+  description: "Default multi-perspective review agent for gerrit."
+  include_filters: {
+    project: "gerrit"
+    path_regex: ".*"
+  }
+  use_default_subagents: true
+  tags: "code-review-agent"
+}
diff --git a/external_deps.lock.json b/external_deps.lock.json
index a5789ad..0bfd35a 100644
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -1,57 +1,57 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
   "__INPUT_ARTIFACTS_HASH": {
-    "antlr:antlr": 1249073166,
-    "aopalliance:aopalliance": -1416324331,
+    "antlr:antlr": 922184171,
+    "aopalliance:aopalliance": 297412442,
     "args4j:args4j": 484840357,
-    "ch.qos.reload4j:reload4j": -1216325797,
-    "com.github.ben-manes.caffeine:caffeine": 1735183231,
-    "com.github.ben-manes.caffeine:guava": -2131703186,
-    "com.github.rholder:guava-retrying": 1309513165,
-    "com.google.auto.factory:auto-factory": -1222857861,
-    "com.google.auto.service:auto-service-annotations": 964333928,
-    "com.google.auto.value:auto-value": -661233359,
-    "com.google.auto.value:auto-value-annotations": 1386235898,
-    "com.google.auto:auto-common": -832702775,
-    "com.google.code.findbugs:jsr305": -1992157670,
-    "com.google.code.gson:gson": 2063056812,
-    "com.google.common.html.types:types": 1474029483,
-    "com.google.errorprone:error_prone_annotations": -1409032738,
+    "ch.qos.reload4j:reload4j": 468466190,
+    "com.github.ben-manes.caffeine:caffeine": -60879720,
+    "com.github.ben-manes.caffeine:guava": -926584503,
+    "com.github.rholder:guava-retrying": -1158425612,
+    "com.google.auto.factory:auto-factory": 2061747342,
+    "com.google.auto.service:auto-service-annotations": 494658103,
+    "com.google.auto.value:auto-value": 1601811244,
+    "com.google.auto.value:auto-value-annotations": 1937464771,
+    "com.google.auto:auto-common": 605821486,
+    "com.google.code.findbugs:jsr305": 1894092877,
+    "com.google.code.gson:gson": 579810017,
+    "com.google.common.html.types:types": -1565521082,
+    "com.google.errorprone:error_prone_annotations": -1370395573,
     "com.google.errorprone:error_prone_type_annotations": -1223112919,
     "com.google.escapevelocity:escapevelocity": -17168624,
-    "com.google.flogger:flogger": -825712792,
-    "com.google.flogger:flogger-log4j-backend": 1730247028,
-    "com.google.flogger:flogger-system-backend": 1522076251,
-    "com.google.flogger:google-extensions": -355130314,
-    "com.google.guava:failureaccess": -2032498474,
-    "com.google.guava:guava": -1756621521,
-    "com.google.guava:guava-testlib": -203887467,
-    "com.google.inject.extensions:guice-assistedinject": -1742632647,
-    "com.google.inject.extensions:guice-servlet": -1284557873,
-    "com.google.inject:guice": 2106361664,
-    "com.google.j2objc:j2objc-annotations": -727464895,
-    "com.google.jimfs:jimfs": -1004381565,
-    "com.google.protobuf:protobuf-java": 130636446,
-    "com.google.template:soy": -843524660,
-    "com.google.truth.extensions:truth-java8-extension": -129319374,
-    "com.google.truth.extensions:truth-liteproto-extension": 1463279446,
-    "com.google.truth.extensions:truth-proto-extension": 1270333764,
-    "com.google.truth:truth": -790731381,
+    "com.google.flogger:flogger": -38159395,
+    "com.google.flogger:flogger-log4j-backend": -1031492015,
+    "com.google.flogger:flogger-system-backend": 1831570954,
+    "com.google.flogger:google-extensions": 1156659279,
+    "com.google.guava:failureaccess": 892280507,
+    "com.google.guava:guava": -506018038,
+    "com.google.guava:guava-testlib": 2001730020,
+    "com.google.inject.extensions:guice-assistedinject": -1633579390,
+    "com.google.inject.extensions:guice-servlet": 1331904556,
+    "com.google.inject:guice": 285885373,
+    "com.google.j2objc:j2objc-annotations": -758748938,
+    "com.google.jimfs:jimfs": 643103458,
+    "com.google.protobuf:protobuf-java": -226930769,
+    "com.google.template:soy": -2011556199,
+    "com.google.truth.extensions:truth-java8-extension": -1498654531,
+    "com.google.truth.extensions:truth-liteproto-extension": 1845738777,
+    "com.google.truth.extensions:truth-proto-extension": -385644437,
+    "com.google.truth:truth": 67900490,
     "com.googlecode.javaewah:JavaEWAH": 1414179411,
-    "com.googlecode.prolog-cafe:prolog-cafeteria": 1472963375,
-    "com.googlecode.prolog-cafe:prolog-compiler": -348984778,
-    "com.googlecode.prolog-cafe:prolog-io": -1081577073,
-    "com.googlecode.prolog-cafe:prolog-runtime": 1849117715,
-    "com.h2database:h2": 377222208,
-    "com.ibm.icu:icu4j": -802150924,
-    "com.icegreen:greenmail": -1151466560,
-    "com.ryanharter.auto.value:auto-value-gson-extension": 520453879,
-    "com.ryanharter.auto.value:auto-value-gson-factory": 1068512450,
-    "com.ryanharter.auto.value:auto-value-gson-runtime": 187755792,
-    "com.squareup:javapoet": 1762437360,
-    "com.sun.mail:javax.mail": -1087889454,
+    "com.googlecode.prolog-cafe:prolog-cafeteria": 540501616,
+    "com.googlecode.prolog-cafe:prolog-compiler": 626105353,
+    "com.googlecode.prolog-cafe:prolog-io": -567526384,
+    "com.googlecode.prolog-cafe:prolog-runtime": 1690151692,
+    "com.h2database:h2": -1803998967,
+    "com.ibm.icu:icu4j": -361890945,
+    "com.icegreen:greenmail": -1688190659,
+    "com.ryanharter.auto.value:auto-value-gson-extension": -949538944,
+    "com.ryanharter.auto.value:auto-value-gson-factory": 476272597,
+    "com.ryanharter.auto.value:auto-value-gson-runtime": -2042171193,
+    "com.squareup:javapoet": 186124283,
+    "com.sun.mail:javax.mail": -860150139,
     "com.vladsch.flexmark:flexmark": -550438914,
-    "com.vladsch.flexmark:flexmark-all": -2127316941,
+    "com.vladsch.flexmark:flexmark-all": 1777926680,
     "com.vladsch.flexmark:flexmark-ext-abbreviation": -475593117,
     "com.vladsch.flexmark:flexmark-ext-admonition": -1153966935,
     "com.vladsch.flexmark:flexmark-ext-anchorlink": 839917508,
@@ -102,109 +102,109 @@
     "commons-beanutils:commons-beanutils": -399736270,
     "commons-codec:commons-codec": -1724312797,
     "commons-collections:commons-collections": -1308259318,
-    "commons-dbcp:commons-dbcp": -873877417,
+    "commons-dbcp:commons-dbcp": 2032916076,
     "commons-digester:commons-digester": 1688456634,
-    "commons-io:commons-io": 1305681826,
+    "commons-io:commons-io": 1226199767,
     "commons-logging:commons-logging": -264381874,
-    "commons-net:commons-net": 1227155931,
-    "commons-pool:commons-pool": -2015226625,
-    "commons-validator:commons-validator": -89255997,
-    "dk.brics:automaton": -733058,
-    "eu.medsea.mimeutil:mime-util": -1364938376,
-    "io.dropwizard.metrics:metrics-core": 1542162680,
-    "io.github.java-diff-utils:java-diff-utils": 1296992014,
-    "io.sweers.autotransient:autotransient": 1591678970,
-    "jakarta.inject:jakarta.inject-api": 190564362,
+    "commons-net:commons-net": 1869273666,
+    "commons-pool:commons-pool": 2048447638,
+    "commons-validator:commons-validator": -577653668,
+    "dk.brics:automaton": -1644468747,
+    "eu.medsea.mimeutil:mime-util": 1812448407,
+    "io.dropwizard.metrics:metrics-core": -921287201,
+    "io.github.java-diff-utils:java-diff-utils": 253797311,
+    "io.sweers.autotransient:autotransient": -1866824261,
+    "jakarta.inject:jakarta.inject-api": -251046605,
     "jakarta.servlet:jakarta.servlet-api": -3550902,
-    "javax.activation:activation": -940616209,
+    "javax.activation:activation": 357577168,
     "javax.annotation:jsr250-api": -593155132,
-    "javax.inject:javax.inject": -297932879,
-    "javax.servlet:javax.servlet-api": 669233360,
-    "junit:junit": -744267592,
+    "javax.inject:javax.inject": -1699438632,
+    "javax.servlet:javax.servlet-api": -1317726989,
+    "junit:junit": 1633899997,
     "log4j:log4j": 182326902,
-    "net.bytebuddy:byte-buddy": 612145055,
-    "net.bytebuddy:byte-buddy-agent": -48125545,
+    "net.bytebuddy:byte-buddy": -2085665942,
+    "net.bytebuddy:byte-buddy-agent": -85049742,
     "net.java.dev.jna:jna": -867910362,
     "net.java.dev.jna:jna-platform": -561312286,
-    "net.minidev:json-smart": -1043043954,
+    "net.minidev:json-smart": -1322128591,
     "net.sf.jopt-simple:jopt-simple": 906822697,
-    "net.sourceforge.nekohtml:nekohtml": 1723624706,
-    "org.antlr:antlr": -2055062274,
-    "org.antlr:antlr-runtime": -2145792567,
-    "org.antlr:stringtemplate": -752719922,
+    "net.sourceforge.nekohtml:nekohtml": 1344054753,
+    "org.antlr:antlr": -1950345125,
+    "org.antlr:antlr-runtime": 2041808304,
+    "org.antlr:stringtemplate": 1543798403,
     "org.apache.ant:ant": 51716534,
-    "org.apache.commons:commons-compress": -1289113474,
+    "org.apache.commons:commons-compress": -314451781,
     "org.apache.commons:commons-lang3": 1729335886,
     "org.apache.commons:commons-math3": -1738699872,
-    "org.apache.commons:commons-text": -1886494041,
-    "org.apache.httpcomponents:fluent-hc": 58615850,
-    "org.apache.httpcomponents:httpclient": 1643860045,
-    "org.apache.httpcomponents:httpcore": -1696303652,
-    "org.apache.james:apache-mime4j-core": -753273784,
-    "org.apache.james:apache-mime4j-dom": -1889888125,
-    "org.apache.lucene:lucene-analysis-common": 1272813182,
-    "org.apache.lucene:lucene-backward-codecs": 2105178323,
-    "org.apache.lucene:lucene-core": 990479611,
+    "org.apache.commons:commons-text": 1055279470,
+    "org.apache.httpcomponents:fluent-hc": -439690549,
+    "org.apache.httpcomponents:httpclient": -214613560,
+    "org.apache.httpcomponents:httpcore": -215175681,
+    "org.apache.james:apache-mime4j-core": -1199514077,
+    "org.apache.james:apache-mime4j-dom": 602022408,
+    "org.apache.lucene:lucene-analysis-common": -894438603,
+    "org.apache.lucene:lucene-backward-codecs": 1670381248,
+    "org.apache.lucene:lucene-core": 217510296,
     "org.apache.lucene:lucene-facet": -125344464,
-    "org.apache.lucene:lucene-misc": -1066196978,
+    "org.apache.lucene:lucene-misc": -2101583963,
     "org.apache.lucene:lucene-queries": 1341801569,
-    "org.apache.lucene:lucene-queryparser": -1514200525,
+    "org.apache.lucene:lucene-queryparser": -536798880,
     "org.apache.lucene:lucene-sandbox": 1803509280,
-    "org.apache.mina:mina-core": -1403307213,
+    "org.apache.mina:mina-core": 91220592,
     "org.apache.sshd:sshd-common": 1844247612,
     "org.apache.sshd:sshd-core": 602454664,
-    "org.apache.sshd:sshd-mina": 582747848,
-    "org.apache.sshd:sshd-osgi": -115413327,
-    "org.apache.sshd:sshd-sftp": 292810504,
-    "org.asciidoctor:asciidoctorj": -457860213,
+    "org.apache.sshd:sshd-mina": 1572508113,
+    "org.apache.sshd:sshd-osgi": -1294702776,
+    "org.apache.sshd:sshd-sftp": -2141240943,
+    "org.asciidoctor:asciidoctorj": -432449150,
     "org.assertj:assertj-core": -1145412507,
-    "org.bouncycastle:bcpg-jdk18on": 915299298,
-    "org.bouncycastle:bcpkix-jdk18on": -1660815403,
-    "org.bouncycastle:bcprov-jdk18on": -1559819624,
-    "org.bouncycastle:bcutil-jdk18on": 2018001773,
-    "org.commonmark:commonmark": 1129543740,
-    "org.commonmark:commonmark-ext-autolink": -1853742120,
-    "org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
-    "org.commonmark:commonmark-ext-gfm-tables": 1881582931,
-    "org.commonmark:commonmark-ext-yaml-front-matter": -1519651186,
+    "org.bouncycastle:bcpg-jdk18on": 219577418,
+    "org.bouncycastle:bcpkix-jdk18on": 254639479,
+    "org.bouncycastle:bcprov-jdk18on": 1844858264,
+    "org.bouncycastle:bcutil-jdk18on": -468935969,
+    "org.commonmark:commonmark": 1195706039,
+    "org.commonmark:commonmark-ext-autolink": -14213733,
+    "org.commonmark:commonmark-ext-gfm-strikethrough": -1869090916,
+    "org.commonmark:commonmark-ext-gfm-tables": -1669467072,
+    "org.commonmark:commonmark-ext-yaml-front-matter": -1752796379,
     "org.eclipse.jetty.ee11:jetty-ee11-servlet": 1925464069,
-    "org.eclipse.jetty.ee8:jetty-ee8-nested": -947438461,
-    "org.eclipse.jetty.ee8:jetty-ee8-security": -1982637940,
-    "org.eclipse.jetty.ee8:jetty-ee8-servlet": -658639341,
-    "org.eclipse.jetty:jetty-http": 444148373,
-    "org.eclipse.jetty:jetty-io": 635179443,
-    "org.eclipse.jetty:jetty-jmx": -1833865362,
-    "org.eclipse.jetty:jetty-security": -1139114931,
-    "org.eclipse.jetty:jetty-server": 549886160,
-    "org.eclipse.jetty:jetty-session": -698327697,
-    "org.eclipse.jetty:jetty-util": 235435535,
-    "org.eclipse.jetty:jetty-util-ajax": 57972836,
-    "org.hamcrest:hamcrest": 1547523135,
-    "org.jruby:jruby-complete": -2103568068,
+    "org.eclipse.jetty.ee8:jetty-ee8-nested": -1747372328,
+    "org.eclipse.jetty.ee8:jetty-ee8-security": 305144879,
+    "org.eclipse.jetty.ee8:jetty-ee8-servlet": 1574335304,
+    "org.eclipse.jetty:jetty-http": -543146126,
+    "org.eclipse.jetty:jetty-io": 845485972,
+    "org.eclipse.jetty:jetty-jmx": -1584909831,
+    "org.eclipse.jetty:jetty-security": 562389690,
+    "org.eclipse.jetty:jetty-server": -2137228969,
+    "org.eclipse.jetty:jetty-session": -1878500776,
+    "org.eclipse.jetty:jetty-util": -1734394440,
+    "org.eclipse.jetty:jetty-util-ajax": 1525156163,
+    "org.hamcrest:hamcrest": -1704983590,
+    "org.jruby:jruby-complete": 544978997,
     "org.json:json": -811907600,
-    "org.jsoup:jsoup": -1061087167,
-    "org.mockito:mockito-core": 1330163800,
-    "org.nibor.autolink:autolink": -342487050,
-    "org.objenesis:objenesis": 748376655,
-    "org.openid4java:openid4java": -1889179110,
+    "org.jsoup:jsoup": -185962132,
+    "org.mockito:mockito-core": -1229333582,
+    "org.nibor.autolink:autolink": 1339824623,
+    "org.objenesis:objenesis": -1132991882,
+    "org.openid4java:openid4java": 1322498581,
     "org.openjdk.jmh:jmh-core": 983716932,
     "org.openjdk.jmh:jmh-generator-annprocess": -1162360421,
-    "org.ow2.asm:asm": 1206815935,
-    "org.ow2.asm:asm-analysis": 53497832,
-    "org.ow2.asm:asm-commons": 1607605466,
-    "org.ow2.asm:asm-tree": -1365652182,
-    "org.ow2.asm:asm-util": 838800782,
-    "org.roaringbitmap:RoaringBitmap": -1626007605,
-    "org.roaringbitmap:shims": 1581618356,
-    "org.slf4j:jcl-over-slf4j": 1978020546,
-    "org.slf4j:slf4j-api": -2005163028,
-    "org.slf4j:slf4j-ext": -916772347,
+    "org.ow2.asm:asm": -68037120,
+    "org.ow2.asm:asm-analysis": 776374327,
+    "org.ow2.asm:asm-commons": 299229701,
+    "org.ow2.asm:asm-tree": 1302540213,
+    "org.ow2.asm:asm-util": 2107679953,
+    "org.roaringbitmap:RoaringBitmap": -1733921478,
+    "org.roaringbitmap:shims": -510093903,
+    "org.slf4j:jcl-over-slf4j": -1147569706,
+    "org.slf4j:slf4j-api": -1626237844,
+    "org.slf4j:slf4j-ext": 1893088691,
     "org.slf4j:slf4j-log4j12": -630224096,
-    "org.slf4j:slf4j-reload4j": 1021466445,
+    "org.slf4j:slf4j-reload4j": -1037943957,
     "org.slf4j:slf4j-simple": 1999565066,
-    "org.tukaani:xz": -1743168321,
+    "org.tukaani:xz": 521927096,
     "repositories": 2019057769,
-    "xerces:xercesImpl": -1165914651,
+    "xerces:xercesImpl": -358868296,
     "xml-apis:xml-apis": -113825062
   },
   "__RESOLVED_ARTIFACTS_HASH": {
@@ -294,7 +294,7 @@
     "com.h2database:h2:jar:sources": 1071831572,
     "com.ibm.icu:icu4j": -1725310050,
     "com.ibm.icu:icu4j:jar:sources": 163265109,
-    "com.icegreen:greenmail": 945965666,
+    "com.icegreen:greenmail": -151631445,
     "com.icegreen:greenmail:jar:sources": -245613146,
     "com.ryanharter.auto.value:auto-value-gson-extension": -1071085858,
     "com.ryanharter.auto.value:auto-value-gson-extension:jar:sources": -670606081,
@@ -317,12 +317,12 @@
     "commons-net:commons-net:jar:sources": -729248524,
     "commons-pool:commons-pool": -212519749,
     "commons-pool:commons-pool:jar:sources": 1134824914,
-    "commons-validator:commons-validator": 298719241,
-    "commons-validator:commons-validator:jar:sources": 1666065747,
+    "commons-validator:commons-validator": -842137273,
+    "commons-validator:commons-validator:jar:sources": 1073953409,
     "dk.brics:automaton": -509279352,
     "dk.brics:automaton:jar:sources": 1035320481,
-    "eu.medsea.mimeutil:mime-util": -188093271,
-    "io.dropwizard.metrics:metrics-core": 740461012,
+    "eu.medsea.mimeutil:mime-util": -947720124,
+    "io.dropwizard.metrics:metrics-core": 2067658873,
     "io.dropwizard.metrics:metrics-core:jar:sources": -1988318320,
     "io.github.java-diff-utils:java-diff-utils": -1180802694,
     "io.github.java-diff-utils:java-diff-utils:jar:sources": 1551226070,
@@ -340,10 +340,10 @@
     "javax.servlet:javax.servlet-api:jar:sources": -2015355058,
     "junit:junit": -1256429642,
     "junit:junit:jar:sources": 940567721,
-    "net.bytebuddy:byte-buddy": 570606503,
-    "net.bytebuddy:byte-buddy-agent": -647040865,
-    "net.bytebuddy:byte-buddy-agent:jar:sources": 1035878534,
-    "net.bytebuddy:byte-buddy:jar:sources": -750344117,
+    "net.bytebuddy:byte-buddy": -1914258003,
+    "net.bytebuddy:byte-buddy-agent": 682658697,
+    "net.bytebuddy:byte-buddy-agent:jar:sources": -1164352937,
+    "net.bytebuddy:byte-buddy:jar:sources": 891177431,
     "net.java.dev.jna:jna": 1622514527,
     "net.java.dev.jna:jna-platform": 1756885266,
     "net.java.dev.jna:jna-platform:jar:sources": -507721531,
@@ -394,26 +394,26 @@
     "org.apache.lucene:lucene-misc:jar:sources": 619654209,
     "org.apache.lucene:lucene-queryparser": -1551455963,
     "org.apache.lucene:lucene-queryparser:jar:sources": -1536444117,
-    "org.apache.mina:mina-core": -584591076,
+    "org.apache.mina:mina-core": -354250191,
     "org.apache.mina:mina-core:jar:sources": 71382131,
-    "org.apache.sshd:sshd-mina": -36716477,
+    "org.apache.sshd:sshd-mina": -641188293,
     "org.apache.sshd:sshd-mina:jar:sources": 97528844,
-    "org.apache.sshd:sshd-osgi": 1810958617,
+    "org.apache.sshd:sshd-osgi": 1183476854,
     "org.apache.sshd:sshd-osgi:jar:sources": 710209222,
-    "org.apache.sshd:sshd-sftp": -320832840,
+    "org.apache.sshd:sshd-sftp": -948314603,
     "org.apache.sshd:sshd-sftp:jar:sources": -1268961386,
     "org.asciidoctor:asciidoctorj": 1685789893,
     "org.asciidoctor:asciidoctorj:jar:sources": 2091708864,
-    "org.assertj:assertj-core": 1185991420,
+    "org.assertj:assertj-core": -253199576,
     "org.assertj:assertj-core:jar:sources": -697161745,
-    "org.bouncycastle:bcpg-jdk18on": 1314692470,
-    "org.bouncycastle:bcpg-jdk18on:jar:sources": -1476258101,
-    "org.bouncycastle:bcpkix-jdk18on": -1562281305,
-    "org.bouncycastle:bcpkix-jdk18on:jar:sources": 1945286569,
-    "org.bouncycastle:bcprov-jdk18on": -390275162,
-    "org.bouncycastle:bcprov-jdk18on:jar:sources": -2118706434,
-    "org.bouncycastle:bcutil-jdk18on": -1194874921,
-    "org.bouncycastle:bcutil-jdk18on:jar:sources": -1005159791,
+    "org.bouncycastle:bcpg-jdk18on": 1344554762,
+    "org.bouncycastle:bcpg-jdk18on:jar:sources": 1086923059,
+    "org.bouncycastle:bcpkix-jdk18on": -287047843,
+    "org.bouncycastle:bcpkix-jdk18on:jar:sources": -1855368494,
+    "org.bouncycastle:bcprov-jdk18on": -751439126,
+    "org.bouncycastle:bcprov-jdk18on:jar:sources": 485448997,
+    "org.bouncycastle:bcutil-jdk18on": -1269559261,
+    "org.bouncycastle:bcutil-jdk18on:jar:sources": 1897597974,
     "org.checkerframework:checker-compat-qual": -1467964223,
     "org.checkerframework:checker-compat-qual:jar:sources": 187825033,
     "org.checkerframework:checker-qual": -739034920,
@@ -428,30 +428,30 @@
     "org.commonmark:commonmark-ext-yaml-front-matter": 1918089254,
     "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources": -902462472,
     "org.commonmark:commonmark:jar:sources": -1511261547,
-    "org.eclipse.jetty.ee11:jetty-ee11-servlet": -820847559,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": -1513828134,
     "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources": -873060978,
-    "org.eclipse.jetty.ee8:jetty-ee8-nested": -159062536,
+    "org.eclipse.jetty.ee8:jetty-ee8-nested": 1812636778,
     "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": 466458242,
-    "org.eclipse.jetty.ee8:jetty-ee8-security": -198386270,
+    "org.eclipse.jetty.ee8:jetty-ee8-security": -294631336,
     "org.eclipse.jetty.ee8:jetty-ee8-security:jar:sources": 494500175,
-    "org.eclipse.jetty.ee8:jetty-ee8-servlet": 130236687,
+    "org.eclipse.jetty.ee8:jetty-ee8-servlet": 1134344934,
     "org.eclipse.jetty.ee8:jetty-ee8-servlet:jar:sources": -1891333550,
     "org.eclipse.jetty.toolchain:jetty-servlet-api": 1364182673,
     "org.eclipse.jetty.toolchain:jetty-servlet-api:jar:sources": 736604807,
-    "org.eclipse.jetty:jetty-http": -328633070,
+    "org.eclipse.jetty:jetty-http": -1581255154,
     "org.eclipse.jetty:jetty-http:jar:sources": 1289113758,
-    "org.eclipse.jetty:jetty-io": -1748418908,
+    "org.eclipse.jetty:jetty-io": 1020373013,
     "org.eclipse.jetty:jetty-io:jar:sources": -444551156,
-    "org.eclipse.jetty:jetty-jmx": 856481565,
+    "org.eclipse.jetty:jetty-jmx": 550212732,
     "org.eclipse.jetty:jetty-jmx:jar:sources": -1523078582,
-    "org.eclipse.jetty:jetty-security": -1524832457,
+    "org.eclipse.jetty:jetty-security": 236529113,
     "org.eclipse.jetty:jetty-security:jar:sources": -272643394,
-    "org.eclipse.jetty:jetty-server": 92951848,
+    "org.eclipse.jetty:jetty-server": 1482573067,
     "org.eclipse.jetty:jetty-server:jar:sources": -1602164191,
-    "org.eclipse.jetty:jetty-session": -1408099417,
+    "org.eclipse.jetty:jetty-session": -1092986007,
     "org.eclipse.jetty:jetty-session:jar:sources": -1187228748,
-    "org.eclipse.jetty:jetty-util": -1243632909,
-    "org.eclipse.jetty:jetty-util-ajax": -818511585,
+    "org.eclipse.jetty:jetty-util": 690289466,
+    "org.eclipse.jetty:jetty-util-ajax": -473650630,
     "org.eclipse.jetty:jetty-util-ajax:jar:sources": -884639814,
     "org.eclipse.jetty:jetty-util:jar:sources": 1776811920,
     "org.hamcrest:hamcrest": 1282317766,
@@ -464,8 +464,8 @@
     "org.jsoup:jsoup:jar:sources": -2058254438,
     "org.jspecify:jspecify": 117231129,
     "org.jspecify:jspecify:jar:sources": -2134060298,
-    "org.mockito:mockito-core": -1701291388,
-    "org.mockito:mockito-core:jar:sources": 1900207417,
+    "org.mockito:mockito-core": -1058138256,
+    "org.mockito:mockito-core:jar:sources": 1799409451,
     "org.nibor.autolink:autolink": -443901116,
     "org.nibor.autolink:autolink:jar:sources": -1863403724,
     "org.objenesis:objenesis": 1536526812,
@@ -490,15 +490,15 @@
     "org.roaringbitmap:RoaringBitmap:jar:sources": -144719403,
     "org.roaringbitmap:shims": -1535336248,
     "org.roaringbitmap:shims:jar:sources": -422658237,
-    "org.slf4j:jcl-over-slf4j": 1113513525,
-    "org.slf4j:jcl-over-slf4j:jar:sources": 494484499,
-    "org.slf4j:slf4j-api": 500377983,
-    "org.slf4j:slf4j-api:jar:sources": 1751644771,
-    "org.slf4j:slf4j-ext": -1625091915,
-    "org.slf4j:slf4j-ext:jar:sources": 194861628,
-    "org.slf4j:slf4j-reload4j": -1779685465,
-    "org.slf4j:slf4j-reload4j:jar:sources": -1558574593,
-    "org.slf4j:slf4j-simple": -1055624668,
+    "org.slf4j:jcl-over-slf4j": 654857378,
+    "org.slf4j:jcl-over-slf4j:jar:sources": 1447796761,
+    "org.slf4j:slf4j-api": -736053657,
+    "org.slf4j:slf4j-api:jar:sources": -72528589,
+    "org.slf4j:slf4j-ext": 1208467804,
+    "org.slf4j:slf4j-ext:jar:sources": 1632735534,
+    "org.slf4j:slf4j-reload4j": 1168851309,
+    "org.slf4j:slf4j-reload4j:jar:sources": 2075068386,
+    "org.slf4j:slf4j-simple": -2071389655,
     "org.slf4j:slf4j-simple:jar:sources": 751828941,
     "org.tukaani:xz": -321288404,
     "org.tukaani:xz:jar:sources": -893332662,
@@ -897,10 +897,10 @@
     },
     "commons-validator:commons-validator": {
       "shasums": {
-        "jar": "bd62795d7068a69cbea333f6dbf9c9c1a6ad7521443fb57202a44874f240ba25",
-        "sources": "9d4d052237a3b010138b853d8603d996cc3f89a6b3f793c5a50b93481cd8dea2"
+        "jar": "b7e1dcdc60d581de82934f4600e25af9ad9366730af1704ddcbf459e4dcc85e5",
+        "sources": "77a063f693602793cee0b64a2ebc066ce9120f2be966146b3b187fa6f921cbd4"
       },
-      "version": "1.6"
+      "version": "1.11.0"
     },
     "dk.brics:automaton": {
       "shasums": {
@@ -980,17 +980,17 @@
     },
     "net.bytebuddy:byte-buddy": {
       "shasums": {
-        "jar": "2ed11da684a8f5b088e0222baa87461cd757e433a8dc4a03671457229d91d5fa",
-        "sources": "b1d35137942a8bff4199b8a2baa00ed3872923bc6878a6ff2731f9324fe1a2fc"
+        "jar": "b7f2b2f111c6ddc09d3d9718ca5895dde16217bb4e5f2d2a6eb571f274909a82",
+        "sources": "00560ac03d67ad6278741a87fc9046c39972daea9ab5ff3378e1b6df3fce7d40"
       },
-      "version": "1.18.12"
+      "version": "1.18.14"
     },
     "net.bytebuddy:byte-buddy-agent": {
       "shasums": {
-        "jar": "287703bea3473edf89e1b373a097e49bad04ed48b85796228f68b1474dc93cf8",
-        "sources": "d4a1b7ab677479288a48889da66ee6e5a76b331ed3fbfb832568b7fc01c6c094"
+        "jar": "6f9159206c4c7950c7ce571b534dcc6457cc27b960146724fbe70c552dd55b97",
+        "sources": "9f52d0860ad2a867af1183f9fc7a4bd41a84a1a8f4b7e69fd4f09aa61968fe95"
       },
-      "version": "1.18.12"
+      "version": "1.18.14"
     },
     "net.java.dev.jna:jna": {
       "shasums": {
@@ -1211,31 +1211,31 @@
     },
     "org.bouncycastle:bcpg-jdk18on": {
       "shasums": {
-        "jar": "39426367dc247dfaae9b1253457a39298b6b9a6c28a39b908bca829e315323dd",
-        "sources": "605b09bd826d7e4e3e0cb4faf2e9b53a6bcb452be963c91499b79fd8497685be"
+        "jar": "8a52ea45b2063f6d41b8ca94409f255a1f4604922f7451c18a7cfd3a3c5b85ab",
+        "sources": "02c36d0c102abbae4ce5c81449cf66338f6befcec2844fc07d685c412e20f1d6"
       },
-      "version": "1.85"
+      "version": "1.86"
     },
     "org.bouncycastle:bcpkix-jdk18on": {
       "shasums": {
-        "jar": "c9f82b2d4e99c4bbdfccf684e52cc06ea06a0b567bfd0d08f9c5a3f417055996",
-        "sources": "e5331f467331aba29bda6ddfb0df0da6d568928e29c2b0f20ea2fe123d802d20"
+        "jar": "8d8b41a4b149bdae8d331f059a578870954d1e3ff29f8269a9eb3ee19a7e4ef7",
+        "sources": "0bb00b50a80404e40c6465cb8012d3835d2a9100d94d16eaa9efed4587d7e34d"
       },
-      "version": "1.85"
+      "version": "1.86"
     },
     "org.bouncycastle:bcprov-jdk18on": {
       "shasums": {
-        "jar": "986b0fb92ec10e0c66b43e036ce0077e6150cfaecd1db9fb92b56672e157afe5",
-        "sources": "b37ac84b1d5435ab7b8d166c16ab9f75e09f68f8ec50479bae433939b241b03f"
+        "jar": "2af190b300cbb0b35e248ccf5f4a06b6072030aeb3da7a98ec73abe5b4cb371f",
+        "sources": "34a8b7dcb030a08ddd5356d3d5be962f9299ca5275e1460273186f8fdbe44c34"
       },
-      "version": "1.85.2"
+      "version": "1.86"
     },
     "org.bouncycastle:bcutil-jdk18on": {
       "shasums": {
-        "jar": "590f55ed5d68529239898a4a5c4f730b6e37f45d1cfa3fbe51f8485abe32c42d",
-        "sources": "b470a692878f92abf00b9c3af9147a45453250a80930df8f23f1d092c55e2d5e"
+        "jar": "1c268e15f785aafb1e4670d8d6e5f856f69475e96c162dadcd1ff9b350e9139f",
+        "sources": "a79e8cdbc04a24aecf64503eda9a4aa61019132484c64f7ce32a68ac30ef6df7"
       },
-      "version": "1.85"
+      "version": "1.86"
     },
     "org.checkerframework:checker-compat-qual": {
       "shasums": {
@@ -1414,10 +1414,10 @@
     },
     "org.mockito:mockito-core": {
       "shasums": {
-        "jar": "ae295bebd5d11fab97ab297815dc7617188b86003cbce3dfd5c0d5c3a6cc4a0c",
-        "sources": "38f869a12a1168ca8dc2a4acd090d8129a7f81afc8f1fc61582701894755fefd"
+        "jar": "97a1f835ad84075255aa37809e4379eff541273c02c6201cbad49bd5950932cd",
+        "sources": "6941ad79b3a92ba2dbd8a00570b0e2dcaf15529de4452c789454f11660f03bf7"
       },
-      "version": "5.23.0"
+      "version": "5.24.0"
     },
     "org.nibor.autolink:autolink": {
       "shasums": {
@@ -1505,31 +1505,31 @@
     },
     "org.slf4j:jcl-over-slf4j": {
       "shasums": {
-        "jar": "ec5c36d57d2dbe100f84878f944575cb7871da32c7a115b8d7e0721277045d67",
-        "sources": "d0a96716020e44f95f1d94c4d2deeda009056847a125453010d4258db6b53776"
+        "jar": "32f95628a4baa60ea8a5b052c8cb6e51d07b0fee3e6092414070874f69daf710",
+        "sources": "16a77e50e2ec588004671e07336c66fb606612e520dd3531be944e345258c212"
       },
-      "version": "2.0.18"
+      "version": "2.0.20"
     },
     "org.slf4j:slf4j-api": {
       "shasums": {
-        "jar": "44508fd1576500688c790b190acdd16fec4f8c79a3e0b900afd70503cf055f55",
-        "sources": "192e007cf7f2be41d40574e44521fc0b7ce55e01f13dbe0fa8707c8ae3329075"
+        "jar": "7e1446499b359675d8aabe6af2de86b85e72ae8a2a932c0b40d0e5ba57097439",
+        "sources": "565e509928d095e20c5d93ca5e3a528e98fc7410c3ca4e84b05ccfadf21b40ba"
       },
-      "version": "2.0.18"
+      "version": "2.0.20"
     },
     "org.slf4j:slf4j-ext": {
       "shasums": {
-        "jar": "75b7945f29a915c0bb984b195d9e60eaca51a57f0b4a2617a8e1f8bc4a4b1b98",
-        "sources": "e60d1f5e66607a182d10597f2d67b7d6db5f10ffbfd4459ee2a7b4eedba5db4c"
+        "jar": "aea68c153bebc2aedd63f07c2391e8a1fd87a4e2a6d086fe787f1d5abc31824d",
+        "sources": "413f1f693016ff7239eb4fb5bf4fad90709c6ded719f305b5b9f91bc3a558e38"
       },
-      "version": "2.0.18"
+      "version": "2.0.20"
     },
     "org.slf4j:slf4j-reload4j": {
       "shasums": {
-        "jar": "37dc7cb891448c3c0db454d9e84c54b3f4e6a5f84b0db35917e7d0f322ae60df",
-        "sources": "b9ce8823a81f3fdc44e017c25ac78ca856cc1cddef1bf85b05f0cbea84b7d57f"
+        "jar": "194277130731fe0ef2a7237759f31a71dc62c116aa4fce26effeb371576ee85b",
+        "sources": "77079cbb0c1dffd8be7e72263c490885054efa7b2958417ef9c1ad7c55014a78"
       },
-      "version": "2.0.18"
+      "version": "2.0.20"
     },
     "org.slf4j:slf4j-simple": {
       "shasums": {
@@ -3418,6 +3418,9 @@
       "org.bouncycastle.openpgp.api.bc",
       "org.bouncycastle.openpgp.api.exception",
       "org.bouncycastle.openpgp.api.jcajce",
+      "org.bouncycastle.openpgp.api.operator",
+      "org.bouncycastle.openpgp.api.operator.bc",
+      "org.bouncycastle.openpgp.api.operator.jcajce",
       "org.bouncycastle.openpgp.api.util",
       "org.bouncycastle.openpgp.bc",
       "org.bouncycastle.openpgp.jcajce",
@@ -3429,6 +3432,9 @@
       "org.bouncycastle.cades",
       "org.bouncycastle.cert",
       "org.bouncycastle.cert.bc",
+      "org.bouncycastle.cert.c509",
+      "org.bouncycastle.cert.c509.bc",
+      "org.bouncycastle.cert.c509.jcajce",
       "org.bouncycastle.cert.cmp",
       "org.bouncycastle.cert.crmf",
       "org.bouncycastle.cert.crmf.bc",
@@ -3542,8 +3548,10 @@
       "org.bouncycastle.crypto.prng",
       "org.bouncycastle.crypto.prng.drbg",
       "org.bouncycastle.crypto.signers",
+      "org.bouncycastle.crypto.signers.lms",
       "org.bouncycastle.crypto.signers.mldsa",
       "org.bouncycastle.crypto.signers.slhdsa",
+      "org.bouncycastle.crypto.signers.xmss",
       "org.bouncycastle.crypto.threshold",
       "org.bouncycastle.crypto.tls",
       "org.bouncycastle.crypto.util",
@@ -3587,6 +3595,7 @@
       "org.bouncycastle.jcajce.provider.asymmetric.mlkem",
       "org.bouncycastle.jcajce.provider.asymmetric.rsa",
       "org.bouncycastle.jcajce.provider.asymmetric.slhdsa",
+      "org.bouncycastle.jcajce.provider.asymmetric.sm9",
       "org.bouncycastle.jcajce.provider.asymmetric.util",
       "org.bouncycastle.jcajce.provider.asymmetric.x509",
       "org.bouncycastle.jcajce.provider.config",
@@ -3621,24 +3630,19 @@
       "org.bouncycastle.math.ec.endo",
       "org.bouncycastle.math.ec.rfc7748",
       "org.bouncycastle.math.ec.rfc8032",
+      "org.bouncycastle.math.ec.sm9",
       "org.bouncycastle.math.ec.tools",
       "org.bouncycastle.math.field",
       "org.bouncycastle.math.raw",
       "org.bouncycastle.pqc.asn1",
       "org.bouncycastle.pqc.crypto",
       "org.bouncycastle.pqc.crypto.aimer",
-      "org.bouncycastle.pqc.crypto.cmce",
-      "org.bouncycastle.pqc.crypto.crystals.dilithium",
       "org.bouncycastle.pqc.crypto.faest",
       "org.bouncycastle.pqc.crypto.falcon",
-      "org.bouncycastle.pqc.crypto.frodo",
       "org.bouncycastle.pqc.crypto.haetae",
-      "org.bouncycastle.pqc.crypto.hawk",
       "org.bouncycastle.pqc.crypto.hqc",
       "org.bouncycastle.pqc.crypto.lms",
       "org.bouncycastle.pqc.crypto.mayo",
-      "org.bouncycastle.pqc.crypto.mldsa",
-      "org.bouncycastle.pqc.crypto.mlkem",
       "org.bouncycastle.pqc.crypto.mqom",
       "org.bouncycastle.pqc.crypto.newhope",
       "org.bouncycastle.pqc.crypto.ntru",
@@ -3647,7 +3651,7 @@
       "org.bouncycastle.pqc.crypto.qruov",
       "org.bouncycastle.pqc.crypto.saber",
       "org.bouncycastle.pqc.crypto.sdith",
-      "org.bouncycastle.pqc.crypto.slhdsa",
+      "org.bouncycastle.pqc.crypto.smaugt",
       "org.bouncycastle.pqc.crypto.snova",
       "org.bouncycastle.pqc.crypto.sphincs",
       "org.bouncycastle.pqc.crypto.sqisign",
@@ -3659,15 +3663,10 @@
       "org.bouncycastle.pqc.jcajce.provider",
       "org.bouncycastle.pqc.jcajce.provider.aimer",
       "org.bouncycastle.pqc.jcajce.provider.bike",
-      "org.bouncycastle.pqc.jcajce.provider.cmce",
-      "org.bouncycastle.pqc.jcajce.provider.dilithium",
       "org.bouncycastle.pqc.jcajce.provider.faest",
       "org.bouncycastle.pqc.jcajce.provider.falcon",
-      "org.bouncycastle.pqc.jcajce.provider.frodo",
       "org.bouncycastle.pqc.jcajce.provider.haetae",
-      "org.bouncycastle.pqc.jcajce.provider.hawk",
       "org.bouncycastle.pqc.jcajce.provider.hqc",
-      "org.bouncycastle.pqc.jcajce.provider.kyber",
       "org.bouncycastle.pqc.jcajce.provider.lms",
       "org.bouncycastle.pqc.jcajce.provider.mayo",
       "org.bouncycastle.pqc.jcajce.provider.mqom",
@@ -3675,21 +3674,20 @@
       "org.bouncycastle.pqc.jcajce.provider.ntru",
       "org.bouncycastle.pqc.jcajce.provider.ntruplus",
       "org.bouncycastle.pqc.jcajce.provider.ntruprime",
-      "org.bouncycastle.pqc.jcajce.provider.picnic",
       "org.bouncycastle.pqc.jcajce.provider.qruov",
       "org.bouncycastle.pqc.jcajce.provider.saber",
       "org.bouncycastle.pqc.jcajce.provider.sdith",
+      "org.bouncycastle.pqc.jcajce.provider.smaugt",
       "org.bouncycastle.pqc.jcajce.provider.snova",
       "org.bouncycastle.pqc.jcajce.provider.sphincs",
-      "org.bouncycastle.pqc.jcajce.provider.sphincsplus",
       "org.bouncycastle.pqc.jcajce.provider.sqisign",
       "org.bouncycastle.pqc.jcajce.provider.uov",
       "org.bouncycastle.pqc.jcajce.provider.util",
       "org.bouncycastle.pqc.jcajce.provider.xmss",
       "org.bouncycastle.pqc.jcajce.spec",
       "org.bouncycastle.pqc.legacy.bike",
-      "org.bouncycastle.pqc.legacy.picnic",
-      "org.bouncycastle.pqc.legacy.rainbow",
+      "org.bouncycastle.pqc.legacy.crystals.dilithium",
+      "org.bouncycastle.pqc.legacy.hawk",
       "org.bouncycastle.pqc.legacy.sphincsplus",
       "org.bouncycastle.pqc.math.ntru",
       "org.bouncycastle.pqc.math.ntru.parameters",
@@ -3734,6 +3732,8 @@
       "org.bouncycastle.asn1.rosstandart",
       "org.bouncycastle.asn1.smime",
       "org.bouncycastle.asn1.tsp",
+      "org.bouncycastle.cbor",
+      "org.bouncycastle.cbor.c509",
       "org.bouncycastle.oer",
       "org.bouncycastle.oer.its",
       "org.bouncycastle.oer.its.etsi102941",
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a0d0907..e33aea6 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -306,7 +306,7 @@
 
   @Inject private AbstractChangeNotes.Args changeNotesArgs;
   @Inject private AccountIndexer accountIndexer;
-  @Inject private EventRecorder.Factory eventRecorderFactory;
+  @Inject protected EventRecorder.Factory eventRecorderFactory;
   @Inject private InProcessProtocol inProcessProtocol;
   @Inject private PluginGuiceEnvironment pluginGuiceEnvironment;
   @Inject private PluginUser.Factory pluginUserFactory;
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index cd66870..dac7e34 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -151,8 +151,8 @@
                     : header));
       }
       EmailHeader titleHeader = message.headers().get("Subject");
-      if (titleHeader instanceof StringEmailHeader) {
-        emailTitle = ((StringEmailHeader) titleHeader).getString();
+      if (titleHeader instanceof StringEmailHeader s) {
+        emailTitle = s.getString();
       }
 
       return this;
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 3fc33e3..5f30814 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -90,8 +90,6 @@
 java_library(
     name = "lib",
     testonly = True,
-    resource_strip_prefix = "resources",
-    resources = ["//resources/com/google/gerrit/acceptance"],
     visibility = ["//visibility:public"],
     exports = [
         ":framework-lib",
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 702b3b4..adae2c1 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.events.ReviewerAddedEvent;
 import com.google.gerrit.server.events.ReviewerDeletedEvent;
 import com.google.gerrit.server.events.UserScopedEventListener;
 import com.google.inject.Inject;
@@ -38,7 +39,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 
-public class EventRecorder {
+public class EventRecorder implements AutoCloseable {
   private final RegistrationHandle eventListenerRegistration;
   private final ListMultimap<String, Event> recordedEvents;
 
@@ -69,12 +70,13 @@
             new UserScopedEventListener() {
               @Override
               public void onEvent(Event e) {
-                if (e instanceof ReviewerDeletedEvent) {
+                if (e instanceof ReviewerAddedEvent) {
+                  recordedEvents.put(ReviewerAddedEvent.TYPE, e);
+                } else if (e instanceof ReviewerDeletedEvent) {
                   recordedEvents.put(ReviewerDeletedEvent.TYPE, e);
                 } else if (e instanceof ChangeDeletedEvent) {
                   recordedEvents.put(ChangeDeletedEvent.TYPE, e);
-                } else if (e instanceof RefEvent) {
-                  RefEvent event = (RefEvent) e;
+                } else if (e instanceof RefEvent event) {
                   String key =
                       refEventKey(
                           event.getType(), event.getProjectNameKey().get(), event.getRefName());
@@ -130,6 +132,21 @@
     return events;
   }
 
+  private ImmutableList<ReviewerAddedEvent> getReviewerAddedEvents(int expectedSize) {
+    String key = ReviewerAddedEvent.TYPE;
+    if (expectedSize == 0) {
+      assertThat(recordedEvents).doesNotContainKey(key);
+      return ImmutableList.of();
+    }
+    assertThat(recordedEvents).containsKey(key);
+    ImmutableList<ReviewerAddedEvent> events =
+        FluentIterable.from(recordedEvents.get(key))
+            .transform(ReviewerAddedEvent.class::cast)
+            .toList();
+    assertThat(events).hasSize(expectedSize);
+    return events;
+  }
+
   private ImmutableList<ReviewerDeletedEvent> getReviewerDeletedEvents(int expectedSize) {
     String key = ReviewerDeletedEvent.TYPE;
     if (expectedSize == 0) {
@@ -219,6 +236,23 @@
     }
   }
 
+  public void assertReviewerAddedEvents(String... expected) {
+    ImmutableList<ReviewerAddedEvent> events = getReviewerAddedEvents(expected.length / 2);
+    int i = 0;
+    for (ReviewerAddedEvent event : events) {
+      String id = event.change.get().id;
+      assertThat(id).isEqualTo(expected[i]);
+      String reviewer = event.reviewer.get().email;
+      assertThat(reviewer).isEqualTo(expected[i + 1]);
+      i += 2;
+    }
+  }
+
+  public void assertNoReviewerAddedEvents() {
+    @SuppressWarnings("unused")
+    var unused = getReviewerAddedEvents(0);
+  }
+
   public void assertReviewerDeletedEvents(String... expected) {
     ImmutableList<ReviewerDeletedEvent> events = getReviewerDeletedEvents(expected.length / 2);
     int i = 0;
@@ -243,6 +277,7 @@
     }
   }
 
+  @Override
   public void close() {
     eventListenerRegistration.remove();
   }
diff --git a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
index 433c149..f01db6b 100644
--- a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
+++ b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
@@ -73,8 +73,8 @@
   @Override
   public void dispatch(AuditEvent action) {
     super.dispatch(action);
-    if (action instanceof HttpAuditEvent) {
-      httpEvents.add((HttpAuditEvent) action);
+    if (action instanceof HttpAuditEvent e) {
+      httpEvents.add(e);
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index a8b01f9..2364f05 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -123,6 +123,11 @@
   @BindingAnnotation
   public @interface TestSshServerAddress {}
 
+  /** Marker on {@link InetSocketAddress} for test HTTP server. */
+  @Retention(RUNTIME)
+  @BindingAnnotation
+  public @interface TestHttpServerAddress {}
+
   @AutoValue
   public abstract static class Description {
     public static Description forTestClass(
@@ -607,7 +612,9 @@
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setBoolean("sendemail", null, "enable", true);
     cfg.setInt("sendemail", null, "threadPoolSize", 0);
-    cfg.setInt("execution", null, "fanOutThreadPoolSize", 0);
+    if (cfg.getString("execution", null, "fanOutThreadPoolSize") == null) {
+      cfg.setInt("execution", null, "fanOutThreadPoolSize", 0);
+    }
     cfg.setInt("plugins", null, "checkFrequency", 0);
 
     cfg.setInt("sshd", null, "threads", 1);
@@ -653,6 +660,14 @@
                 ? SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0)
                 : null;
           }
+
+          @Provides
+          @Singleton
+          @TestHttpServerAddress
+          InetSocketAddress getHttpAddress(@GerritServerConfig Config cfg) {
+            URI uri = URI.create(cfg.getString("httpd", null, "listenUrl"));
+            return new InetSocketAddress(uri.getHost(), uri.getPort());
+          }
         };
     return sysInjector.createChildInjector(module);
   }
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 0da9d30..a85da54 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -334,8 +334,7 @@
   }
 
   private void deleteNewlyCreatedProjects() {
-    if (deleteNewProjects && repoManager instanceof InMemoryRepositoryManager) {
-      InMemoryRepositoryManager inMemoryRepoManager = (InMemoryRepositoryManager) repoManager;
+    if (deleteNewProjects && repoManager instanceof InMemoryRepositoryManager inMemoryRepoManager) {
       Sets.difference(inMemoryRepoManager.list(), savedProjectSet)
           .forEach(
               project -> {
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 783ad86..34eae12 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -64,23 +64,27 @@
   public static final String FILE_NAME = "a.txt";
   public static final String FILE_CONTENT = "some content";
   public static final String PATCH_FILE_ONLY =
-      "diff --git a/a.txt b/a.txt\n"
-          + "new file mode 100644\n"
-          + "index 0000000..f0eec86\n"
-          + "--- /dev/null\n"
-          + "+++ b/a.txt\n"
-          + "@@ -0,0 +1 @@\n"
-          + "+some content\n"
-          + "\\ No newline at end of file\n";
+      """
+      diff --git a/a.txt b/a.txt
+      new file mode 100644
+      index 0000000..f0eec86
+      --- /dev/null
+      +++ b/a.txt
+      @@ -0,0 +1 @@
+      +some content
+      \\ No newline at end of file
+      """;
   public static final String PATCH =
-      "From %s Mon Sep 17 00:00:00 2001\n"
-          + "From: Administrator <admin@example.com>\n"
-          + "Date: %s\n"
-          + "Subject: [PATCH] test commit\n"
-          + "\n"
-          + "Change-Id: %s\n"
-          + "---\n"
-          + "\n"
+      """
+      From %s Mon Sep 17 00:00:00 2001
+      From: Administrator <admin@example.com>
+      Date: %s
+      Subject: [PATCH] test commit
+
+      Change-Id: %s
+      ---
+
+      """
           + PATCH_FILE_ONLY;
 
   public static class Module extends FactoryModule {
@@ -407,8 +411,7 @@
     }
     if (tag != null) {
       TagCommand tagCommand = testRepo.git().tag().setName(tag.name);
-      if (tag instanceof AnnotatedTag) {
-        AnnotatedTag annotatedTag = (AnnotatedTag) tag;
+      if (tag instanceof AnnotatedTag annotatedTag) {
         tagCommand
             .setAnnotated(true)
             .setMessage(annotatedTag.message)
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/IndexOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/IndexOperations.java
index cba9b15..2975fa4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/IndexOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/IndexOperations.java
@@ -62,9 +62,8 @@
 
       return () -> {
         ChangeIndex maybeEnabledSearchIndex = indices.getSearchIndex();
-        if (maybeEnabledSearchIndex instanceof DisabledChangeIndex) {
-          indices.setSearchIndex(
-              ((DisabledChangeIndex) maybeEnabledSearchIndex).unwrap(), /* closeOld */ false);
+        if (maybeEnabledSearchIndex instanceof DisabledChangeIndex disabledChangeIndex) {
+          indices.setSearchIndex(disabledChangeIndex.unwrap(), /* closeOld */ false);
         }
       };
     }
@@ -78,8 +77,8 @@
       }
       return () -> {
         for (ChangeIndex i : indices.getWriteIndexes()) {
-          if (i instanceof DisabledChangeIndex) {
-            indices.addWriteIndex(((DisabledChangeIndex) i).unwrap());
+          if (i instanceof DisabledChangeIndex disabledChangeIndex) {
+            indices.addWriteIndex(disabledChangeIndex.unwrap());
           }
         }
       };
@@ -99,9 +98,8 @@
 
       return () -> {
         AccountIndex maybeEnabledSearchIndex = indices.getSearchIndex();
-        if (maybeEnabledSearchIndex instanceof DisabledAccountIndex) {
-          indices.setSearchIndex(
-              ((DisabledAccountIndex) maybeEnabledSearchIndex).unwrap(), /* closeOld */ false);
+        if (maybeEnabledSearchIndex instanceof DisabledAccountIndex disabledAccountIndex) {
+          indices.setSearchIndex(disabledAccountIndex.unwrap(), /* closeOld */ false);
         }
       };
     }
@@ -115,8 +113,8 @@
       }
       return () -> {
         for (AccountIndex i : indices.getWriteIndexes()) {
-          if (i instanceof DisabledAccountIndex) {
-            indices.addWriteIndex(((DisabledAccountIndex) i).unwrap());
+          if (i instanceof DisabledAccountIndex disabledAccountIndex) {
+            indices.addWriteIndex(disabledAccountIndex.unwrap());
           }
         }
       };
@@ -136,9 +134,8 @@
 
       return () -> {
         ProjectIndex maybeEnabledSearchIndex = indices.getSearchIndex();
-        if (maybeEnabledSearchIndex instanceof DisabledProjectIndex) {
-          indices.setSearchIndex(
-              ((DisabledProjectIndex) maybeEnabledSearchIndex).unwrap(), /* closeOld */ false);
+        if (maybeEnabledSearchIndex instanceof DisabledProjectIndex disabledProjectIndex) {
+          indices.setSearchIndex(disabledProjectIndex.unwrap(), /* closeOld */ false);
         }
       };
     }
@@ -152,8 +149,8 @@
       }
       return () -> {
         for (ProjectIndex i : indices.getWriteIndexes()) {
-          if (i instanceof DisabledProjectIndex) {
-            indices.addWriteIndex(((DisabledProjectIndex) i).unwrap());
+          if (i instanceof DisabledProjectIndex disabledProjectIndex) {
+            indices.addWriteIndex(disabledProjectIndex.unwrap());
           }
         }
       };
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index 03336ca..c5efda1 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -104,8 +104,8 @@
     // 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();
+    if (oldContext instanceof ManualRequestContext manualRequestContext) {
+      manualRequestContext.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
diff --git a/java/com/google/gerrit/auth/ldap/Helper.java b/java/com/google/gerrit/auth/ldap/Helper.java
index 53e8ac5..22fc28b 100644
--- a/java/com/google/gerrit/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/auth/ldap/Helper.java
@@ -36,8 +36,6 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import java.io.IOException;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -45,6 +43,7 @@
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.TimeUnit;
 import javax.naming.CompositeName;
 import javax.naming.Context;
@@ -234,13 +233,13 @@
     }
     Subject subject = ctx.getSubject();
     try {
-      return Subject.doAs(
-          subject, (PrivilegedExceptionAction<DirContext>) () -> createContext(env));
-    } catch (PrivilegedActionException e) {
-      Throwables.throwIfInstanceOf(e.getException(), IOException.class);
-      Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
-      Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
-      logger.atWarning().withCause(e.getException()).log("Internal error");
+      return Subject.callAs(subject, () -> createContext(env));
+    } catch (CompletionException e) {
+      Throwable cause = e.getCause();
+      Throwables.throwIfInstanceOf(cause, IOException.class);
+      Throwables.throwIfInstanceOf(cause, NamingException.class);
+      Throwables.throwIfInstanceOf(cause, RuntimeException.class);
+      logger.atWarning().withCause(cause).log("Internal error");
       return null;
     } finally {
       ctx.logout();
@@ -374,6 +373,7 @@
     return ImmutableSet.copyOf(actual);
   }
 
+  @SuppressWarnings("BanJNDI") // LDAP access is inherently JNDI-based.
   private void recursivelyExpandGroups(
       final Set<String> groupDNs,
       final LdapSchema schema,
diff --git a/java/com/google/gerrit/auth/ldap/LdapQuery.java b/java/com/google/gerrit/auth/ldap/LdapQuery.java
index 71dc141..2d3b2a6 100644
--- a/java/com/google/gerrit/auth/ldap/LdapQuery.java
+++ b/java/com/google/gerrit/auth/ldap/LdapQuery.java
@@ -63,6 +63,7 @@
     return pattern.getParameterNames();
   }
 
+  @SuppressWarnings("BanJNDI") // LDAP access is inherently JNDI-based.
   List<Result> query(DirContext ctx, Map<String, String> params, Timer0 queryTimer)
       throws NamingException {
     final SearchControls sc = new SearchControls();
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index e33e5cb..03a7a1c 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -407,6 +407,7 @@
       this.helper = helper;
     }
 
+    @SuppressWarnings("BanJNDI") // LDAP access is inherently JNDI-based.
     @Override
     public Boolean load(String groupDn) throws Exception {
       try (TraceTimer timer =
diff --git a/java/com/google/gerrit/auth/ldap/LdapType.java b/java/com/google/gerrit/auth/ldap/LdapType.java
index c486335..fbc0118 100644
--- a/java/com/google/gerrit/auth/ldap/LdapType.java
+++ b/java/com/google/gerrit/auth/ldap/LdapType.java
@@ -22,6 +22,7 @@
 abstract class LdapType {
   static final LdapType RFC_2307 = new Rfc2307();
 
+  @SuppressWarnings("BanJNDI") // LDAP access is inherently JNDI-based.
   static LdapType guessType(DirContext ctx) throws NamingException {
     final Attributes rootAtts = ctx.getAttributes("");
     Attribute supported = rootAtts.get("supportedCapabilities");
diff --git a/java/com/google/gerrit/common/PluginData.java b/java/com/google/gerrit/common/PluginData.java
index 289d93a..0ad3074 100644
--- a/java/com/google/gerrit/common/PluginData.java
+++ b/java/com/google/gerrit/common/PluginData.java
@@ -30,8 +30,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof PluginData) {
-      PluginData o = (PluginData) obj;
+    if (obj instanceof PluginData o) {
       return Objects.equals(name, o.name)
           && Objects.equals(version, o.version)
           && Objects.equals(pluginPath, o.pluginPath);
diff --git a/java/com/google/gerrit/entities/Address.java b/java/com/google/gerrit/entities/Address.java
index eb1da46..4e398fe 100644
--- a/java/com/google/gerrit/entities/Address.java
+++ b/java/com/google/gerrit/entities/Address.java
@@ -76,8 +76,8 @@
 
   @Override
   public final boolean equals(Object other) {
-    if (other instanceof Address) {
-      return email().equals(((Address) other).email());
+    if (other instanceof Address a) {
+      return email().equals(a.email());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 3a331c3..a3cf635 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -89,8 +89,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Key) {
-        Key k = (Key) o;
+      if (o instanceof Key k) {
         return Objects.equals(uuid, k.uuid)
             && Objects.equals(filename, k.filename)
             && patchSetId == k.patchSetId;
@@ -117,8 +116,8 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Identity) {
-        return id == ((Identity) o).id;
+      if (o instanceof Identity identity) {
+        return id == identity.id;
       }
       return false;
     }
@@ -183,8 +182,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Range) {
-        Range r = (Range) o;
+      if (o instanceof Range r) {
         return startLine == r.startLine
             && startChar == r.startChar
             && endLine == r.endLine
diff --git a/java/com/google/gerrit/entities/CommentRange.java b/java/com/google/gerrit/entities/CommentRange.java
index f58780f..111a22b 100644
--- a/java/com/google/gerrit/entities/CommentRange.java
+++ b/java/com/google/gerrit/entities/CommentRange.java
@@ -72,8 +72,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof CommentRange) {
-      CommentRange other = (CommentRange) obj;
+    if (obj instanceof CommentRange other) {
       return startLine == other.startLine
           && startCharacter == other.startCharacter
           && endLine == other.endLine
diff --git a/java/com/google/gerrit/entities/LabelTypes.java b/java/com/google/gerrit/entities/LabelTypes.java
index 5807190..f2593eb 100644
--- a/java/com/google/gerrit/entities/LabelTypes.java
+++ b/java/com/google/gerrit/entities/LabelTypes.java
@@ -98,8 +98,7 @@
     if (this == o) {
       return true;
     }
-    if (o instanceof LabelTypes) {
-      LabelTypes other = (LabelTypes) o;
+    if (o instanceof LabelTypes other) {
       return labelTypes.equals(other.labelTypes);
     }
     return false;
diff --git a/java/com/google/gerrit/entities/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
index 42baebc..a970698 100644
--- a/java/com/google/gerrit/entities/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -120,8 +120,8 @@
 
   @Override
   public final boolean equals(Object obj) {
-    if (obj instanceof NotifyConfig) {
-      return compareTo((NotifyConfig) obj) == 0;
+    if (obj instanceof NotifyConfig notifyConfig) {
+      return compareTo(notifyConfig) == 0;
     }
     return false;
   }
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index c212f0c..885edc6 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -60,7 +60,6 @@
   public static final String SUBMIT = "submit";
   public static final String SUBMIT_AS = "submitAs";
   public static final String TOGGLE_WORK_IN_PROGRESS_STATE = "toggleWipState";
-  public static final String POST_REVIEW_COMMENT = "postReviewComment";
   public static final String VIEW_PRIVATE_CHANGES = "viewPrivateChanges";
   public static final String AI_REVIEW = "aiReview";
 
@@ -100,7 +99,6 @@
     NAMES_LC.add(SUBMIT.toLowerCase(Locale.US));
     NAMES_LC.add(SUBMIT_AS.toLowerCase(Locale.US));
     NAMES_LC.add(TOGGLE_WORK_IN_PROGRESS_STATE.toLowerCase(Locale.US));
-    NAMES_LC.add(POST_REVIEW_COMMENT.toLowerCase(Locale.US));
     NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase(Locale.US));
     NAMES_LC.add(AI_REVIEW.toLowerCase(Locale.US));
 
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index a1ccc18..fdfc2a5 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -84,8 +84,8 @@
     }
 
     default boolean projectNameEquals(Object b) {
-      if (b instanceof NameKey) {
-        return name().equals(((NameKey) b).get());
+      if (b instanceof NameKey nameKey) {
+        return name().equals(nameKey.get());
       }
       return false;
     }
diff --git a/java/com/google/gerrit/entities/SubmitRecord.java b/java/com/google/gerrit/entities/SubmitRecord.java
index 4142b42..d92041a 100644
--- a/java/com/google/gerrit/entities/SubmitRecord.java
+++ b/java/com/google/gerrit/entities/SubmitRecord.java
@@ -139,8 +139,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Label) {
-        Label l = (Label) o;
+      if (o instanceof Label l) {
         return Objects.equals(label, l.label)
             && Objects.equals(status, l.status)
             && Objects.equals(appliedBy, l.appliedBy);
@@ -201,8 +200,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof SubmitRecord) {
-      SubmitRecord r = (SubmitRecord) o;
+    if (o instanceof SubmitRecord r) {
       return Objects.equals(status, r.status)
           && Objects.equals(labels, r.labels)
           && Objects.equals(errorMessage, r.errorMessage)
diff --git a/java/com/google/gerrit/entities/SubmoduleSubscription.java b/java/com/google/gerrit/entities/SubmoduleSubscription.java
index db26eb3..4f4d78d 100644
--- a/java/com/google/gerrit/entities/SubmoduleSubscription.java
+++ b/java/com/google/gerrit/entities/SubmoduleSubscription.java
@@ -55,8 +55,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof SubmoduleSubscription) {
-      SubmoduleSubscription s = (SubmoduleSubscription) o;
+    if (o instanceof SubmoduleSubscription s) {
       return superProject.equals(s.superProject)
           && submodulePath.equals(s.submodulePath)
           && submodule.equals(s.submodule);
diff --git a/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java b/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
index 7c47ec5..03a92ce 100644
--- a/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
+++ b/java/com/google/gerrit/extensions/api/access/AccessSectionInfo.java
@@ -22,8 +22,8 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof AccessSectionInfo) {
-      return Objects.equals(permissions, ((AccessSectionInfo) obj).permissions);
+    if (obj instanceof AccessSectionInfo accessSectionInfo) {
+      return Objects.equals(permissions, accessSectionInfo.permissions);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/extensions/api/access/PermissionInfo.java b/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
index c4808a5..0df7027 100644
--- a/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
+++ b/java/com/google/gerrit/extensions/api/access/PermissionInfo.java
@@ -28,8 +28,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof PermissionInfo) {
-      PermissionInfo p = (PermissionInfo) obj;
+    if (obj instanceof PermissionInfo p) {
       return Objects.equals(label, p.label)
           && Objects.equals(exclusive, p.exclusive)
           && Objects.equals(rules, p.rules);
diff --git a/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java b/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
index f979039..a10771e 100644
--- a/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
+++ b/java/com/google/gerrit/extensions/api/access/PermissionRuleInfo.java
@@ -36,8 +36,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof PermissionRuleInfo) {
-      PermissionRuleInfo p = (PermissionRuleInfo) obj;
+    if (obj instanceof PermissionRuleInfo p) {
       return Objects.equals(action, p.action)
           && Objects.equals(force, p.force)
           && Objects.equals(min, p.min)
diff --git a/java/com/google/gerrit/extensions/api/access/PluginPermission.java b/java/com/google/gerrit/extensions/api/access/PluginPermission.java
index f536b2c..5493eca 100644
--- a/java/com/google/gerrit/extensions/api/access/PluginPermission.java
+++ b/java/com/google/gerrit/extensions/api/access/PluginPermission.java
@@ -63,8 +63,7 @@
 
   @Override
   public boolean equals(Object other) {
-    if (other instanceof PluginPermission) {
-      PluginPermission b = (PluginPermission) other;
+    if (other instanceof PluginPermission b) {
       return pluginName.equals(b.pluginName) && capability.equals(b.capability);
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java b/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java
index 8fa1e55..8ec2a9d 100644
--- a/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java
+++ b/java/com/google/gerrit/extensions/api/access/PluginProjectPermission.java
@@ -65,8 +65,7 @@
 
   @Override
   public boolean equals(Object other) {
-    if (other instanceof PluginProjectPermission) {
-      PluginProjectPermission b = (PluginProjectPermission) other;
+    if (other instanceof PluginProjectPermission b) {
       return pluginName.equals(b.pluginName) && permission.equals(b.permission);
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java b/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
index 9fb57ad..88b62de 100644
--- a/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
+++ b/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
@@ -64,8 +64,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof ConsistencyProblemInfo) {
-        ConsistencyProblemInfo other = ((ConsistencyProblemInfo) o);
+      if (o instanceof ConsistencyProblemInfo other) {
         return Objects.equals(status, other.status) && Objects.equals(message, other.message);
       }
       return false;
diff --git a/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java b/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java
index 5c47ac3..31979e6 100644
--- a/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/CommentLinkInfo.java
@@ -33,8 +33,7 @@
     if (o == this) {
       return true;
     }
-    if (o instanceof CommentLinkInfo) {
-      CommentLinkInfo that = (CommentLinkInfo) o;
+    if (o instanceof CommentLinkInfo that) {
       return Objects.equals(this.match, that.match)
           && Objects.equals(this.link, that.link)
           && Objects.equals(this.prefix, that.prefix)
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index 3f33713..b126812 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -94,8 +94,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Range) {
-        Range r = (Range) o;
+      if (o instanceof Range r) {
         return startLine == r.startLine
             && startCharacter == r.startCharacter
             && endLine == r.endLine
diff --git a/java/com/google/gerrit/extensions/client/MenuItem.java b/java/com/google/gerrit/extensions/client/MenuItem.java
index fea885b..5d778b5 100644
--- a/java/com/google/gerrit/extensions/client/MenuItem.java
+++ b/java/com/google/gerrit/extensions/client/MenuItem.java
@@ -41,8 +41,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof MenuItem) {
-      MenuItem o = (MenuItem) obj;
+    if (obj instanceof MenuItem o) {
       return Objects.equals(url, o.url)
           && Objects.equals(name, o.name)
           && Objects.equals(target, o.target)
diff --git a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
index c446d87..0152c5f 100644
--- a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
+++ b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -29,8 +29,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof ProjectWatchInfo) {
-      ProjectWatchInfo w = (ProjectWatchInfo) obj;
+    if (obj instanceof ProjectWatchInfo w) {
       return Objects.equals(project, w.project)
           && Objects.equals(filter, w.filter)
           && Objects.equals(problem, w.problem)
diff --git a/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java b/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
index b51e195..5f1c234 100644
--- a/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
@@ -55,8 +55,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof AccountExternalIdInfo) {
-      AccountExternalIdInfo a = (AccountExternalIdInfo) o;
+    if (o instanceof AccountExternalIdInfo a) {
       return Objects.equals(a.identity, identity)
           && Objects.equals(a.emailAddress, emailAddress)
           && Objects.equals(a.trusted, trusted)
diff --git a/java/com/google/gerrit/extensions/common/AccountInfo.java b/java/com/google/gerrit/extensions/common/AccountInfo.java
index 74d495c..6b4b9bf 100644
--- a/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -109,8 +109,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof AccountInfo) {
-      AccountInfo accountInfo = (AccountInfo) o;
+    if (o instanceof AccountInfo accountInfo) {
       return Objects.equals(_accountId, accountInfo._accountId)
           && Objects.equals(name, accountInfo.name)
           && Objects.equals(displayName, accountInfo.displayName)
diff --git a/java/com/google/gerrit/extensions/common/ActionInfo.java b/java/com/google/gerrit/extensions/common/ActionInfo.java
index f148444..a7d6c3f 100644
--- a/java/com/google/gerrit/extensions/common/ActionInfo.java
+++ b/java/com/google/gerrit/extensions/common/ActionInfo.java
@@ -79,8 +79,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ActionInfo) {
-      ActionInfo actionInfo = (ActionInfo) o;
+    if (o instanceof ActionInfo actionInfo) {
       return Objects.equals(method, actionInfo.method)
           && Objects.equals(label, actionInfo.label)
           && Objects.equals(title, actionInfo.title)
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 9147b51..ad3dcf9 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -100,8 +100,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ApprovalInfo) {
-      ApprovalInfo approvalInfo = (ApprovalInfo) o;
+    if (o instanceof ApprovalInfo approvalInfo) {
       return super.equals(o)
           && Objects.equals(tag, approvalInfo.tag)
           && Objects.equals(value, approvalInfo.value)
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
index b220371..9cd1827 100644
--- a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
+++ b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
@@ -71,8 +71,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof AttentionSetInfo) {
-      AttentionSetInfo attentionSetInfo = (AttentionSetInfo) o;
+    if (o instanceof AttentionSetInfo attentionSetInfo) {
       return Objects.equals(account, attentionSetInfo.account)
           && Objects.equals(lastUpdate, attentionSetInfo.lastUpdate)
           && Objects.equals(reason, attentionSetInfo.reason)
diff --git a/java/com/google/gerrit/extensions/common/AvatarInfo.java b/java/com/google/gerrit/extensions/common/AvatarInfo.java
index 55ae08f..794e198 100644
--- a/java/com/google/gerrit/extensions/common/AvatarInfo.java
+++ b/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -40,8 +40,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof AvatarInfo) {
-      AvatarInfo avatarInfo = (AvatarInfo) o;
+    if (o instanceof AvatarInfo avatarInfo) {
       return Objects.equals(url, avatarInfo.url) && Objects.equals(height, avatarInfo.height);
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 308daf0..e5a2a17 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -74,14 +74,13 @@
 
   @SuppressWarnings("unchecked") // reflection is used to construct instances of T
   private static <T> T getAdded(T oldValue, T newValue) {
-    if (newValue instanceof Collection) {
-      ImmutableList<?> result =
-          getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
+    if (newValue instanceof Collection<?> newCollection) {
+      ImmutableList<?> result = getAddedForCollection((Collection<?>) oldValue, newCollection);
       return (T) result;
     }
 
-    if (newValue instanceof Map) {
-      ImmutableMap<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
+    if (newValue instanceof Map<?, ?> newMap) {
+      ImmutableMap<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, newMap);
       return (T) result;
     }
 
diff --git a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index c128f36..940706e 100644
--- a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -50,8 +50,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ChangeMessageInfo) {
-      ChangeMessageInfo cmi = (ChangeMessageInfo) o;
+    if (o instanceof ChangeMessageInfo cmi) {
       return Objects.equals(id, cmi.id)
           && Objects.equals(tag, cmi.tag)
           && Objects.equals(author, cmi.author)
diff --git a/java/com/google/gerrit/extensions/common/ContextLineInfo.java b/java/com/google/gerrit/extensions/common/ContextLineInfo.java
index 3062e85..d56e8ea 100644
--- a/java/com/google/gerrit/extensions/common/ContextLineInfo.java
+++ b/java/com/google/gerrit/extensions/common/ContextLineInfo.java
@@ -33,8 +33,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ContextLineInfo) {
-      ContextLineInfo l = (ContextLineInfo) o;
+    if (o instanceof ContextLineInfo l) {
       return lineNumber == l.lineNumber && contextLine.equals(l.contextLine);
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/common/FetchInfo.java b/java/com/google/gerrit/extensions/common/FetchInfo.java
index 4b1e941..b35aace 100644
--- a/java/com/google/gerrit/extensions/common/FetchInfo.java
+++ b/java/com/google/gerrit/extensions/common/FetchInfo.java
@@ -29,8 +29,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof FetchInfo) {
-      FetchInfo fetchInfo = (FetchInfo) o;
+    if (o instanceof FetchInfo fetchInfo) {
       return Objects.equals(url, fetchInfo.url)
           && Objects.equals(ref, fetchInfo.ref)
           && Objects.equals(commands, fetchInfo.commands);
diff --git a/java/com/google/gerrit/extensions/common/FileInfo.java b/java/com/google/gerrit/extensions/common/FileInfo.java
index db85ac8..f960fc2 100644
--- a/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -37,8 +37,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof FileInfo) {
-      FileInfo fileInfo = (FileInfo) o;
+    if (o instanceof FileInfo fileInfo) {
       return Objects.equals(status, fileInfo.status)
           && Objects.equals(oldMode, fileInfo.oldMode)
           && Objects.equals(newMode, fileInfo.newMode)
diff --git a/java/com/google/gerrit/extensions/common/GpgKeyInfo.java b/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
index d656f22..4f9e40f 100644
--- a/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
+++ b/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
@@ -47,8 +47,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof GpgKeyInfo) {
-      GpgKeyInfo gpgKeyInfo = (GpgKeyInfo) o;
+    if (o instanceof GpgKeyInfo gpgKeyInfo) {
       return Objects.equals(id, gpgKeyInfo.id)
           && Objects.equals(fingerprint, gpgKeyInfo.fingerprint)
           && Objects.equals(userIds, gpgKeyInfo.userIds)
diff --git a/java/com/google/gerrit/extensions/common/LabelInfo.java b/java/com/google/gerrit/extensions/common/LabelInfo.java
index cdd3b18..cef7cd4 100644
--- a/java/com/google/gerrit/extensions/common/LabelInfo.java
+++ b/java/com/google/gerrit/extensions/common/LabelInfo.java
@@ -35,8 +35,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof LabelInfo) {
-      LabelInfo labelInfo = (LabelInfo) o;
+    if (o instanceof LabelInfo labelInfo) {
       return Objects.equals(approved, labelInfo.approved)
           && Objects.equals(rejected, labelInfo.rejected)
           && Objects.equals(recommended, labelInfo.recommended)
diff --git a/java/com/google/gerrit/extensions/common/MetadataInfo.java b/java/com/google/gerrit/extensions/common/MetadataInfo.java
index 2213191..dea41de 100644
--- a/java/com/google/gerrit/extensions/common/MetadataInfo.java
+++ b/java/com/google/gerrit/extensions/common/MetadataInfo.java
@@ -58,8 +58,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof MetadataInfo) {
-      MetadataInfo metadata = (MetadataInfo) o;
+    if (o instanceof MetadataInfo metadata) {
       return Objects.equals(name, metadata.name)
           && Objects.equals(value, metadata.value)
           && Objects.equals(description, metadata.description)
diff --git a/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java b/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
index e2b1c36..9b3069a 100644
--- a/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
+++ b/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
@@ -22,8 +22,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof PluginDefinedInfo) {
-      PluginDefinedInfo pluginDefinedInfo = (PluginDefinedInfo) o;
+    if (o instanceof PluginDefinedInfo pluginDefinedInfo) {
       return Objects.equals(name, pluginDefinedInfo.name)
           && Objects.equals(message, pluginDefinedInfo.message);
     }
diff --git a/java/com/google/gerrit/extensions/common/PushCertificateInfo.java b/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
index 199dbd1..af4fe8d 100644
--- a/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
+++ b/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
@@ -22,8 +22,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof PushCertificateInfo) {
-      PushCertificateInfo pushCertificateInfo = (PushCertificateInfo) o;
+    if (o instanceof PushCertificateInfo pushCertificateInfo) {
       return Objects.equals(certificate, pushCertificateInfo.certificate)
           && Objects.equals(key, pushCertificateInfo.key);
     }
diff --git a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
index e14da7c..47c124c 100644
--- a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
+++ b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
@@ -49,8 +49,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ReviewerUpdateInfo) {
-      ReviewerUpdateInfo reviewerUpdateInfo = (ReviewerUpdateInfo) o;
+    if (o instanceof ReviewerUpdateInfo reviewerUpdateInfo) {
       return Objects.equals(updated, reviewerUpdateInfo.updated)
           && Objects.equals(updatedBy, reviewerUpdateInfo.updatedBy)
           && Objects.equals(realUpdatedBy, reviewerUpdateInfo.realUpdatedBy)
diff --git a/java/com/google/gerrit/extensions/common/RevisionInfo.java b/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 23e2a26..e85bc68 100644
--- a/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -79,8 +79,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof RevisionInfo) {
-      RevisionInfo revisionInfo = (RevisionInfo) o;
+    if (o instanceof RevisionInfo revisionInfo) {
       return isCurrent == revisionInfo.isCurrent
           && Objects.equals(kind, revisionInfo.kind)
           && _number == revisionInfo._number
@@ -158,8 +157,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof ParentInfo) {
-        ParentInfo parentInfo = (ParentInfo) o;
+      if (o instanceof ParentInfo parentInfo) {
         return Objects.equals(branchName, parentInfo.branchName)
             && Objects.equals(commitId, parentInfo.commitId)
             && Objects.equals(isMergedInTargetBranch, parentInfo.isMergedInTargetBranch)
diff --git a/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java b/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
index 2af9a767..e7f8e6e 100644
--- a/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
+++ b/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
@@ -37,8 +37,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof TestSubmitRuleInfo) {
-      TestSubmitRuleInfo other = (TestSubmitRuleInfo) o;
+    if (o instanceof TestSubmitRuleInfo other) {
       return Objects.equals(status, other.status)
           && Objects.equals(errorMessage, other.errorMessage)
           && Objects.equals(ok, other.ok)
diff --git a/java/com/google/gerrit/extensions/common/TrackingIdInfo.java b/java/com/google/gerrit/extensions/common/TrackingIdInfo.java
index 3d35e08..74ef59e 100644
--- a/java/com/google/gerrit/extensions/common/TrackingIdInfo.java
+++ b/java/com/google/gerrit/extensions/common/TrackingIdInfo.java
@@ -27,8 +27,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof TrackingIdInfo) {
-      TrackingIdInfo trackingIdInfo = (TrackingIdInfo) o;
+    if (o instanceof TrackingIdInfo trackingIdInfo) {
       return Objects.equals(system, trackingIdInfo.system) && Objects.equals(id, trackingIdInfo.id);
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/common/ValidationOptionInfo.java b/java/com/google/gerrit/extensions/common/ValidationOptionInfo.java
index ccbd70f..bddb360 100644
--- a/java/com/google/gerrit/extensions/common/ValidationOptionInfo.java
+++ b/java/com/google/gerrit/extensions/common/ValidationOptionInfo.java
@@ -28,8 +28,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof ValidationOptionInfo) {
-      ValidationOptionInfo validationInfo = (ValidationOptionInfo) o;
+    if (o instanceof ValidationOptionInfo validationInfo) {
       return Objects.equals(name, validationInfo.name)
           && Objects.equals(description, validationInfo.description);
     }
diff --git a/java/com/google/gerrit/extensions/common/VotingRangeInfo.java b/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
index 2f7e9e4..1e37d80 100644
--- a/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
@@ -27,8 +27,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof VotingRangeInfo) {
-      VotingRangeInfo votingRangeInfo = (VotingRangeInfo) o;
+    if (o instanceof VotingRangeInfo votingRangeInfo) {
       return min == votingRangeInfo.min && max == votingRangeInfo.max;
     }
     return false;
diff --git a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
index fde4d23..55cb5fc 100644
--- a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
+++ b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
@@ -143,8 +143,7 @@
 
     @Override
     public boolean equals(Object other) {
-      if (other instanceof And) {
-        And o = (And) other;
+      if (other instanceof And o) {
         return a.equals(o.a) && b.equals(o.b);
       }
       return false;
@@ -201,8 +200,7 @@
 
     @Override
     public boolean equals(Object other) {
-      if (other instanceof Or) {
-        Or o = (Or) other;
+      if (other instanceof Or o) {
         return a.equals(o.a) && b.equals(o.b);
       }
       return false;
diff --git a/java/com/google/gerrit/extensions/registration/DynamicMap.java b/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 6fd2c03..3d90a7d 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -181,8 +181,7 @@
 
     @Override
     public boolean equals(Object other) {
-      if (other instanceof NamePair) {
-        NamePair np = (NamePair) other;
+      if (other instanceof NamePair np) {
         return pluginName.equals(np.pluginName) && exportName.equals(np.exportName);
       }
       return false;
diff --git a/java/com/google/gerrit/extensions/restapi/IdString.java b/java/com/google/gerrit/extensions/restapi/IdString.java
index a69919f..b18cf63 100644
--- a/java/com/google/gerrit/extensions/restapi/IdString.java
+++ b/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -67,8 +67,8 @@
 
   @Override
   public boolean equals(Object other) {
-    if (other instanceof IdString) {
-      return urlEncoded.equals(((IdString) other).urlEncoded);
+    if (other instanceof IdString idString) {
+      return urlEncoded.equals(idString.urlEncoded);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 7659943..eced737 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -259,8 +259,7 @@
         }
         foundAtLeastOneKey = true;
         Object obj = it.next();
-        if (obj instanceof PGPPublicKeyRing) {
-          PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
+        if (obj instanceof PGPPublicKeyRing kr) {
           if (fp == null || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
             keys.add(kr);
           }
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index b9ff50b..ac7f9e4 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -184,8 +184,7 @@
     PGPObjectFactory factory = new BcPGPObjectFactory(in);
     Object obj;
     while ((obj = factory.nextObject()) != null) {
-      if (obj instanceof PGPSignatureList) {
-        PGPSignatureList sigs = (PGPSignatureList) obj;
+      if (obj instanceof PGPSignatureList sigs) {
         if (!sigs.isEmpty()) {
           return sigs.get(0);
         }
diff --git a/java/com/google/gerrit/gpg/testing/TestKeys.java b/java/com/google/gerrit/gpg/testing/TestKeys.java
index 0423474..9365577 100644
--- a/java/com/google/gerrit/gpg/testing/TestKeys.java
+++ b/java/com/google/gerrit/gpg/testing/TestKeys.java
@@ -35,93 +35,97 @@
    */
   public static TestKey validKeyWithoutExpiration() {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
-            + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
-            + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
-            + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
-            + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
-            + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
-            + "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
-            + "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
-            + "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
-            + "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
-            + "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
-            + "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
-            + "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
-            + "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
-            + "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
-            + "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
-            + "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
-            + "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
-            + "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
-            + "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
-            + "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
-            + "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
-            + "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
-            + "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
-            + "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
-            + "=o/aU\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
-            + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
-            + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
-            + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
-            + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
-            + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAEAB/wLoOXEJ+Buo+OZHjpb\n"
-            + "SSZf8GdGs+mOJoKbSJvR6zT/rFsrikUvOPmgt8B9qWjKmJVXO5L09+/Wd/MuX0L1\n"
-            + "7plhdvowP1bl2/j5VyLvZx2qwKXkiCGStFzrBGp9nKtJp4Z8O69pb//ZXaiAtDJC\n"
-            + "HFa1kYT4VgFTevrXtg/z/C0np4Yjx0mZpw4nfISEeHCiYCyRa/B8R1+Pc4uIcoSo\n"
-            + "G3aq6Ow9m/LGvw0MRO5qHvqoF41TLPQpGKjKEsCBKHF1qh0tOOUHnLGrvbmdFnGr\n"
-            + "UXJpRkLdRTnj8ufvA4XVZhImzL+lD+ALtjlV14xh8nsNKYL42880GFl5Cl0OtBcE\n"
-            + "lgQBBADPJ6kHdvUYOe0zugRdukBSYLkZcYwRiphom7dZuavYICIu6B14ljEONzVD\n"
-            + "mPhi2lDOawZOURKwYd9S4K11XWLsTYe7XEwkc+1Fpvu4L/JqnJTTnnvbx05ZsqD5\n"
-            + "j9tybPlrTuLrf2ctfcC03Z55wfo6azsbf89yrr6QX0+l9dlkYQQA/xcMdQJ0Z5vm\n"
-            + "kvyaCPsQzJc/8noVO9PMv7xJm14gJWK7Px3y2eBidzpCbVVFnGWW6CPb3qKerB5U\n"
-            + "pwcF4gCFWyP9C2YtnB0hgqixIPfR+UO8gpqdY6MP8NPspoXouffRn+Zic/P6Cxje\n"
-            + "/MGxNQBeRtqb2IGh1xZ8v/8tmmmxHIkEAP74HkGETcXmlj3/6RlwTBUAovPARSn7\n"
-            + "LDtOCPezg6mQmble1BvnTnAwOHKJVqjx+3qsGqMe8OGGXAxZPSU1xSmOShBFrpDp\n"
-            + "xArE67arE17pT1lyD/gmHRuqnNMvgRrwz1mDm3G2ohWkCVixEiB+8vPQfbZrJBgQ\n"
-            + "WxOF4RCo2WWyRKa0IFRlc3R1c2VyIE9uZSA8dGVzdDFAZXhhbXBsZS5jb20+iQE4\n"
-            + "BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDtBiXc\n"
-            + "RjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8LqyUpBrDp3P06QDGpKGFMAovBuh+NL\n"
-            + "H76VKNIzQLQC8rdTj651fLcLMuJ1enQ3RblgRKr1oc+wqqtFHr4QyOQjE/N3C9GQ\n"
-            + "jEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMxjRcHbM9KQnsE5Z4fh4wmN5ynG+5n\n"
-            + "baF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX7Qkzze+scAlc9E/EWRJQIFcxnxV/\n"
-            + "SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjyW0lGHnh/ZqH6XGVcGUaJZZ2uHTck\n"
-            + "1+czuVVShNcXPW1W20T6E9UqzHbJHN0gnQOYBFWdTIkBCACoLVdPr3gpQwzI+2NG\n"
-            + "XjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjNvYkS/+/oGtVEmiYOiAVTwmkjCYkK\n"
-            + "GDgNcCiJVekiPAN6JryVv488wRc999b5LpFEfhLGwI0YxjcS4KFFnpMC3wSb6tJU\n"
-            + "nHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIbnuyrk3ydEcS4ZeGD+w+taIxMc9F1\n"
-            + "DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3mrBCo97sE95yKcq98ZMIWuQtTcEcc\n"
-            + "ZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11VlIQ9QFSj6ruqoKrYvNZuDDLD1lHvZ\n"
-            + "PD4/ABEBAAEAB/4kQnJauehcbRpqktjaqSGmP9HFSp+50CyZbLUJJM8m0uyQsZMr\n"
-            + "k9JQOZc+Q3RERNTKj7m41Fbhsj7c0Qd856/eJdp3kdBME0hko8lxN/X4EWGjeLYe\n"
-            + "z41+iPgfZhCF0Oa66TecPQ5RRihGPaDPoVPpkmMWMt9L7KVviBg1eJ6bobVIY5hu\n"
-            + "a7KFJHZQcCI1OvdJ0cx89KDSbnH8iMM6Kmw1bE3D2FEaWctuKLBo5PNRgyTJvdBd\n"
-            + "PSf56/Rc6csPqmOntQi2Yn8n47eCOTclHNuygSTJeHPpymVuWbhMq6fhJat/xA+V\n"
-            + "kyT8I2c45RQb0dKId+wEytjbKw8AI6Q3GXqhBADOhsr9M+JWc4MpD43mCDZACN4v\n"
-            + "RBRxSrJvO/V6HqQPmKYRmr9Gk3vxgF0zCf5zB1QeBiXpTpShxV87RIbUYReOyavp\n"
-            + "87zH6/SkRxQJiBEpQh5Fu5CoAaxGOivxbPqdWHrBY6jvqkrRoMPNiFJ6/ty5w9jx\n"
-            + "i9kGm9PelQGu2SdLNwQA0HbGo8sC8h5TSTEDCkFHRYzVYONx+32AlkCsJX9mEt0E\n"
-            + "nG8d97Ay24JsbnuXSq04FJrqzjOVyHLUffpXnAGELJZVNCIparSyqIaj43UG/oPc\n"
-            + "ICPmR7zI9G49ICUPSzI7+S2+BwjbiHRQcP0zmxbH92G4abYwKfk7dsDpGyVM+TkD\n"
-            + "/2nUiV0CRqnGipeiLWNjW/Md0ufkwqBvCWxrtxj0rQCyvBOVg3B6DocVNzgOOYa1\n"
-            + "ji3We5A9mSP40JBmMfk2veFrDdsGn4G+OpzMxKQtNfYemqjALfZ2zTdax0mXPXy6\n"
-            + "Gl0jUgSGrxGm8QnRLsrRx7G7ZKnvkcS+YsdQ8dbtzvJtQfiJAR8EGAECAAkFAlWd\n"
-            + "TIkCGwwACgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUs\n"
-            + "j/16fGiFrRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl\n"
-            + "+xqsgpEjFhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs\n"
-            + "3YI19Ci/FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnx\n"
-            + "qhH4wfHBPGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1\n"
-            + "H2PPSxrA0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
-            + "=MuAn\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr
+        atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w
+        va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V
+        DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro
+        7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p
+        9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8
+        dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ
+        CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq
+        yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg
+        RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx
+        jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX
+        7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy
+        W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN
+        BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN
+        vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE
+        fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb
+        nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m
+        rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl
+        IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA
+        CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF
+        rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj
+        Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/
+        FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB
+        PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA
+        0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==
+        =o/aU
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =MuAn
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -136,94 +140,98 @@
    */
   public static final TestKey validKeyWithExpiration() {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
-            + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
-            + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
-            + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
-            + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
-            + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
-            + "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
-            + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
-            + "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
-            + "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
-            + "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
-            + "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
-            + "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
-            + "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
-            + "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
-            + "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
-            + "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
-            + "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
-            + "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
-            + "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
-            + "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
-            + "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
-            + "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
-            + "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
-            + "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
-            + "=1e/A\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
-            + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
-            + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
-            + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
-            + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
-            + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAEAB/0WW33OVqzEBwj9b/3X\n"
-            + "i+75I/Gb+yVtDZ/km2NwSJie33PirE4mTNKitTBkt1oxmphw5Yqji4gEkI/rXcqy\n"
-            + "OcY/fCIZ+gVT+yE2MCPF7Se4Tnl7tSvPxoUn6mOQ09AygyYVjlSCY02EAL/WxwUH\n"
-            + "6OCs6VYlNiBlPg7O2vHGzlzAd1aMmlG3ytlhb0SIbilaJn/wlQ2SEGySjIAP1qRH\n"
-            + "UXsTfW7oAjdqAY1CbCWg/0FnMBF+DnChH634dbLrS2OefcB70l61trEfRcHbMNTv\n"
-            + "9nVxDDCpaIdxsOfgWpe0GMG1qddRAxBIOVjNUFOL22xEFyaXnt/uagUtKQ7yejci\n"
-            + "bgTFBADcuhsfQaBX1G095iG2qr8Rx2T5GqNf9oZA+rbweWegqIH7MUXHI1KKwwJx\n"
-            + "C+rR5AgnxTSP614XI/AWB/txdelm8z0jLobpS6B1vzM2vRQ7hpwjJ3UvUkoQ5uYL\n"
-            + "DjaBqQi0w1cPJA79H0Yujc1zgdhATymz0uDL1BC2bHLIMuhelwQA80p07G1w8HLQ\n"
-            + "bTdgNwtDBMKIw39/ZyQy8ppxmpD4J6zf25r95g3er0r+njrHsa+72LnvexbedpKA\n"
-            + "4eiDJPN+l5jJOEWfL2WtGcqJ01bdFBPcl73tuwDJJtieUlKZH0jRjykuuUX8F+tJ\n"
-            + "yrmVoIGtawoeLKq3hMMOK4xi+sh3OrcD+wXIU24eO3YfUde5bhyaQplNMU5smIU0\n"
-            + "+looOEmFsZcTONgoN+FKrnm2TY9d4FHZ+QgtnksWHmmLxQJPtp9rHJ5BgdxMBPcK\n"
-            + "3w5GXRuWlOmqmnAb6vp0Q0yzVDLKCcwba0S23m3tbjZsLDcI7MG/knsp9gtL676D\n"
-            + "AsrpeF2+Apj0OwG0IFRlc3R1c2VyIFR3byA8dGVzdDJAZXhhbXBsZS5jb20+iQE+\n"
-            + "BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\n"
-            + "CRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0dUdvAXeBx7DwOAnAodis9ZVqChb7R\n"
-            + "xcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6bgW+1WOB1tZSDVxwL1PnZFw/SyAD\n"
-            + "RIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZFMTFUr2SPscXk1k7muS+ZfEFwNPD\n"
-            + "4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT449CYoq8XBMBfvyWl/LLpw0r3JI6\n"
-            + "pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T8TKDGwwiuwiiT3SfkFSVdcjKulRu\n"
-            + "XSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iuRHSOnQOYBFWdTP8BCADhhGxAA0pX\n"
-            + "5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnRtBScgKZnP0sjRTYEUIwmZuseHMBo\n"
-            + "htVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIeqCrm/6aejbFcQOpxe6U29KJRCAxu\n"
-            + "wNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZoIvpIe9tZH4aXitCY2MCQH+hTyCy\n"
-            + "NBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+97HCe042GIq65h0apgujyjhJidjch\n"
-            + "5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xPd9MncY5Q/eH+hn96694k5bckottS\n"
-            + "yGm/3f2Ihfj1ABEBAAEAB/wP5H+mcTTrhe+57sEHuo9bQDocG+3fMtesHlRCept6\n"
-            + "vg1VQG4Va2GOtCCs7yMz4aNGz4jxOdB7bUkZJyFiRehG0+ahWi5b9JbSegf46Nm2\n"
-            + "54vt4icH2WtaEB04JaD/91k4yrunnzwVEAVDmhhIzjf4KbEjPLeBA7rF7zb0Gexq\n"
-            + "mdxEGO/6KdeQ6KOxkpWEqIIdl/mAGsYCprHeKL/XL+KXYr92nEbUcltmt59TTnoo\n"
-            + "00BQCPuHCdpcUd5nuaxpCZLM+BEpxtj0sinz0ofuWU9RI4K00R01MKXWMucdOhTZ\n"
-            + "kUy5dMx8wA07xbjkE/nH86N76Mty133OB7G3lBBDfO4PBADulfLzbjXUnS1kTKeP\n"
-            + "j/HF1E9qafzTDS/QD55OVajDq66A6zaOazKbURHNZmIqpLO4715+iNtrZQUEP3e1\n"
-            + "mwngeizvAv9luA9kJ1YDTCfsS5H5cYzavhfwuqBu7fQBm/PQqZplQuPCxgXEIBaY\n"
-            + "M0uvR0I/FSwFrepRN2IA6dAkrwQA8fpJEg8C9OLFzDf0rxV3eWwEelemN4E50Obu\n"
-            + "nxtg9IJWZ+QIWkRVLJ8if5+p85s2ieCw8hzEF0FyNfWUnfW5eoN4/j50loR4EbZS\n"
-            + "qOpUJGwr8ezyQN8PpduDOe9OQnUYAv9FY9Rk46L4937GDF2w5gdxyNdKO8yG+Z3A\n"
-            + "6/0DLZsEAOQsRUXIl1XLjkdugfFQ8V9Fv3AYWJt+8zknwcQ+Z3uOtyY2muCi9hX2\n"
-            + "BtuPojjwmN6x8wntMaUkzYHVSdz/cdx+na7VNS2kZHfnECWZGR6IHyRTJN5612yi\n"
-            + "e4MIdTE+BgL1HPq+VIPlMBehEksC5qM0WSq8baMsacGMYeAL8ntoRuyJASUEGAEC\n"
-            + "AA8FAlWdTP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb\n"
-            + "1nsgRMgVYoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFy\n"
-            + "xo6lLHw9NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q\n"
-            + "3uwvP5fbfSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfq\n"
-            + "lOG7SPvMNmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk1\n"
-            + "3ynADO+vEOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN\n"
-            + "9A==\n"
-            + "=qbV3\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3
+        vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj
+        3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV
+        rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe
+        IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P
+        inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8
+        dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD
+        AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d
+        UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6
+        bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ
+        FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT
+        449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T
+        8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu
+        RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR
+        tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe
+        qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ
+        oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9
+        7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP
+        d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd
+        TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV
+        YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9
+        NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb
+        fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM
+        NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v
+        EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==
+        =1e/A
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        lQOYBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3
+        vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj
+        3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV
+        rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe
+        IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P
+        inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAEAB/0WW33OVqzEBwj9b/3X
+        i+75I/Gb+yVtDZ/km2NwSJie33PirE4mTNKitTBkt1oxmphw5Yqji4gEkI/rXcqy
+        OcY/fCIZ+gVT+yE2MCPF7Se4Tnl7tSvPxoUn6mOQ09AygyYVjlSCY02EAL/WxwUH
+        6OCs6VYlNiBlPg7O2vHGzlzAd1aMmlG3ytlhb0SIbilaJn/wlQ2SEGySjIAP1qRH
+        UXsTfW7oAjdqAY1CbCWg/0FnMBF+DnChH634dbLrS2OefcB70l61trEfRcHbMNTv
+        9nVxDDCpaIdxsOfgWpe0GMG1qddRAxBIOVjNUFOL22xEFyaXnt/uagUtKQ7yejci
+        bgTFBADcuhsfQaBX1G095iG2qr8Rx2T5GqNf9oZA+rbweWegqIH7MUXHI1KKwwJx
+        C+rR5AgnxTSP614XI/AWB/txdelm8z0jLobpS6B1vzM2vRQ7hpwjJ3UvUkoQ5uYL
+        DjaBqQi0w1cPJA79H0Yujc1zgdhATymz0uDL1BC2bHLIMuhelwQA80p07G1w8HLQ
+        bTdgNwtDBMKIw39/ZyQy8ppxmpD4J6zf25r95g3er0r+njrHsa+72LnvexbedpKA
+        4eiDJPN+l5jJOEWfL2WtGcqJ01bdFBPcl73tuwDJJtieUlKZH0jRjykuuUX8F+tJ
+        yrmVoIGtawoeLKq3hMMOK4xi+sh3OrcD+wXIU24eO3YfUde5bhyaQplNMU5smIU0
+        +looOEmFsZcTONgoN+FKrnm2TY9d4FHZ+QgtnksWHmmLxQJPtp9rHJ5BgdxMBPcK
+        3w5GXRuWlOmqmnAb6vp0Q0yzVDLKCcwba0S23m3tbjZsLDcI7MG/knsp9gtL676D
+        AsrpeF2+Apj0OwG0IFRlc3R1c2VyIFR3byA8dGVzdDJAZXhhbXBsZS5jb20+iQE+
+        BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK
+        CRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0dUdvAXeBx7DwOAnAodis9ZVqChb7R
+        xcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6bgW+1WOB1tZSDVxwL1PnZFw/SyAD
+        RIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZFMTFUr2SPscXk1k7muS+ZfEFwNPD
+        4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT449CYoq8XBMBfvyWl/LLpw0r3JI6
+        pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T8TKDGwwiuwiiT3SfkFSVdcjKulRu
+        XSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iuRHSOnQOYBFWdTP8BCADhhGxAA0pX
+        5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnRtBScgKZnP0sjRTYEUIwmZuseHMBo
+        htVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIeqCrm/6aejbFcQOpxe6U29KJRCAxu
+        wNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZoIvpIe9tZH4aXitCY2MCQH+hTyCy
+        NBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+97HCe042GIq65h0apgujyjhJidjch
+        5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xPd9MncY5Q/eH+hn96694k5bckottS
+        yGm/3f2Ihfj1ABEBAAEAB/wP5H+mcTTrhe+57sEHuo9bQDocG+3fMtesHlRCept6
+        vg1VQG4Va2GOtCCs7yMz4aNGz4jxOdB7bUkZJyFiRehG0+ahWi5b9JbSegf46Nm2
+        54vt4icH2WtaEB04JaD/91k4yrunnzwVEAVDmhhIzjf4KbEjPLeBA7rF7zb0Gexq
+        mdxEGO/6KdeQ6KOxkpWEqIIdl/mAGsYCprHeKL/XL+KXYr92nEbUcltmt59TTnoo
+        00BQCPuHCdpcUd5nuaxpCZLM+BEpxtj0sinz0ofuWU9RI4K00R01MKXWMucdOhTZ
+        kUy5dMx8wA07xbjkE/nH86N76Mty133OB7G3lBBDfO4PBADulfLzbjXUnS1kTKeP
+        j/HF1E9qafzTDS/QD55OVajDq66A6zaOazKbURHNZmIqpLO4715+iNtrZQUEP3e1
+        mwngeizvAv9luA9kJ1YDTCfsS5H5cYzavhfwuqBu7fQBm/PQqZplQuPCxgXEIBaY
+        M0uvR0I/FSwFrepRN2IA6dAkrwQA8fpJEg8C9OLFzDf0rxV3eWwEelemN4E50Obu
+        nxtg9IJWZ+QIWkRVLJ8if5+p85s2ieCw8hzEF0FyNfWUnfW5eoN4/j50loR4EbZS
+        qOpUJGwr8ezyQN8PpduDOe9OQnUYAv9FY9Rk46L4937GDF2w5gdxyNdKO8yG+Z3A
+        6/0DLZsEAOQsRUXIl1XLjkdugfFQ8V9Fv3AYWJt+8zknwcQ+Z3uOtyY2muCi9hX2
+        BtuPojjwmN6x8wntMaUkzYHVSdz/cdx+na7VNS2kZHfnECWZGR6IHyRTJN5612yi
+        e4MIdTE+BgL1HPq+VIPlMBehEksC5qM0WSq8baMsacGMYeAL8ntoRuyJASUEGAEC
+        AA8FAlWdTP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb
+        1nsgRMgVYoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFy
+        xo6lLHw9NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q
+        3uwvP5fbfSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfq
+        lOG7SPvMNmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk1
+        3ynADO+vEOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN
+        9A==
+        =qbV3
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -237,94 +245,98 @@
    */
   public static final TestKey expiredKey() {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
-            + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
-            + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
-            + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
-            + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
-            + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
-            + "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
-            + "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
-            + "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
-            + "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
-            + "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
-            + "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
-            + "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
-            + "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
-            + "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
-            + "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
-            + "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
-            + "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
-            + "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
-            + "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
-            + "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
-            + "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
-            + "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
-            + "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
-            + "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
-            + "=d/Xp\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
-            + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
-            + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
-            + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
-            + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
-            + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAEAB/4hGI3ckkLMTjRVa7G1\n"
-            + "YYSv4sr8dHXz0CVpZXKOo+Stef3Z4pZTK/BcXOdROvaXooD+EheAs6Yn4fpnT+/K\n"
-            + "IB7ZAx6C0OL8vz17gbPuBFltMZ/COUwaCi/gFCUfWQgqRp/SdHaOfCIuTxpAkDSS\n"
-            + "tpmWJ8eDDSFudMpgweb+SrF9DkCwp+FgUbzDRzO1aqzuu8PGihCHQt/pkhNHQ63/\n"
-            + "srDDqk6lIxxZHhv9+ucr3plDuijkvAa5/QDudQlucKDLtTPSD40UcqYnpg/V/RJU\n"
-            + "eBK0ZXmCIHpG9beHW/xdlwrK3eY4Z2sVDMm9TeeHmRYOCr5wQCyeLpMdAt0Ijk6a\n"
-            + "nINhBADI2lRodgnLvUKbOvVocz8WQjG1IXlL8iXSNuuHONijPXZiWh7XdkNxr9fm\n"
-            + "jRqzvZzYsWGT6MnirX2eXaEWJsWJHxTxJuiuOk0V/iGnV/d+jFduoKXNmB5k/ZB3\n"
-            + "6zySi7+STKNyIvnMATVsRoI/cNUwfmx53m6trFg581CnSiA82QQA4kSPw9OXmTKj\n"
-            + "ctlHrWsapWu+66pDVZw62lW6lvrd7t+m8liNb6VJuTnwIKVXJOQtUo1+GSMs0+YK\n"
-            + "wnd9FGq4jT8l0qBO4K/8B1HxppLC2S0ntC+CusxWMUDbdC2xg+G2W3oLwq3iamgz\n"
-            + "LvPTy1Pzs9PqDd6FXIdzieFy6J8W1+sEAKS3vjh7Z/PIVULZhdaohAd5Igd67S/Z\n"
-            + "BMWYNbBuJTnnb7DiOllLZSd2lR7IAKPKsUd6UY8uskOxI81hI116zNx17mIGFIIq\n"
-            + "DdDgRbvzMNEgNlOxg/BD01kXOS4fhnT2F6ca3VGTgUtOdcdF3M9MtePWQLBzEDPz\n"
-            + "8nx3O20HDupuQmG0IlRlc3R1c2VyIFRocmVlIDx0ZXN0M0BleGFtcGxlLmNvbT6J\n"
-            + "AT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheA\n"
-            + "AAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFCECWLrcOeimuvwbmkonNzOkvKbGXl\n"
-            + "73GStISAksRWAHBQED1rEPC0NkFCDeVZO7dfSYLlsqKwV6uSh05Ra0F5XeniC12Y\n"
-            + "pAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCuR+8sNu/oecMRcFK4S9NaApi3vdqB\n"
-            + "NhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSkqcPfKZmocNXdgLV5Q80n3hc2y2nr\n"
-            + "l+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5btBW2L0UHtoEyiqkRfD6lX2laSLQ\n"
-            + "mA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/2thO41KdA5gEQs6nRQEIAM/833UH\n"
-            + "K1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3beeE4sh1NG5DbRCdo6iacZLarWr3FD\n"
-            + "z7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5Fp5u2R4WF546bWqX45xPdLfHVTPyW\n"
-            + "B9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihwdxLsxaga+QmaL0bAR+dRcO6ucj7T\n"
-            + "DQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9AjFoumMZ6l+k30sSdjSjpBMsNvPos0\n"
-            + "dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELpKgujZ2sKC9Nm395u6Q4cqUWihzb/\n"
-            + "Y7rIRuNHJarI7vUAEQEAAQAH+gNBKDf7FDzwdM37Sz8Ej7OsPcIbekzPcOpV3mzM\n"
-            + "u/NIuOY0QSvW7KRE8hwFlXjVZocJU/Z4Qqw+12pN55LusiRUrOq8eKuJIbl4QikI\n"
-            + "Dea8XUqM+CKJPV3YZXs6YVdIuzrRBSLgsB/Glff5JlzkEjsRYVmmnto8edETL/MK\n"
-            + "S9ClJqQiFKE4b01+Eh9oB/DfxzsiEf/a+rdRnWRh/jtpEwgeXcfmjhf+0zrzChu2\n"
-            + "ylQQ5QOuwQNKJP6DvRu/W5pOaKH9tPDR31SccDJDdnDUzBD7oSsXl06DcfMNEa8q\n"
-            + "PaNHLDDRNnqTEhwYSJ4r2emDFMxg7Kky+aatUNjAYk9vkgMEANnvumgr6/KCLWKc\n"
-            + "D3fZE09N7BveGBBDQBYNGPFtx60WbKrSY3e2RSfgWbyEXkzwm1VlB2869T1we0rL\n"
-            + "z6eV/TK5rrJQxJFHZ/anMxbQY0sCiOgqi6PKT03RTpA2N803hTym+oypy+5T6BFM\n"
-            + "rtjXvwIZN/BgAE2JjA70crTAd1mvBAD0UFNAU9oE7K7sgDbni4EhxmDyaviBHfxV\n"
-            + "PJP1ICUXAcEzAsz2T/L5TqZUD+LfYIkbf8wk2/mPZFfrCrQgCrzWn7KV1SHXkhf4\n"
-            + "4Sg6Y6p0g0Jl3mWRPiQ6ALlOVQIkp5V8z4b0hTF2c4oct1Pzaeq+ZkahyvrhW06P\n"
-            + "iaucRZb+mwP/aVTpkd4n/FyKCcbf9/KniYJ+Ou1OunsBQr/jzN+r0PKCb8l/ksig\n"
-            + "i/M0NGetemq9CxYsJDAyJs1aO4SWgx5LbfcMmyXDuJ3sL0ztFLOES31Mih3ZJebg\n"
-            + "xPpj2bB/67i2zeYRcjxQ116y23gOa2TWM8EE4TW7F/mQjw4fIPJ93ClBMIkBJQQY\n"
-            + "AQIADwUCQs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs\n"
-            + "7mvEWJI/1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Fe\n"
-            + "yxb2rjtbNrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt1\n"
-            + "0RaYR8VEZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGd\n"
-            + "Ut8U1Kq9OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/\n"
-            + "jPj5FUEUkE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6Q\n"
-            + "HDJb\n"
-            + "=RrXv\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H
+        55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99
+        uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X
+        Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV
+        cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc
+        RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl
+        IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI
+        BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC
+        ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df
+        SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu
+        R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk
+        qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5
+        btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/
+        2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be
+        eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F
+        p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw
+        dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj
+        FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp
+        KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC
+        Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/
+        1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb
+        NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE
+        ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9
+        OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU
+        kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb
+        =d/Xp
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        HDJb
+        =RrXv
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -338,99 +350,103 @@
    */
   public static final TestKey selfRevokedKey() {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP\n"
-            + "Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm\n"
-            + "qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y\n"
-            + "gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB\n"
-            + "7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR\n"
-            + "uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAGJAR8EIAECAAkFAlWdVXkC\n"
-            + "HQIACgkQCFUqF3yoeCH4lgf/aBdTYqnwL1lreHbQaUXI0/B2zlMuoptoi/x+xjIB\n"
-            + "7RszzaN3w0n4/87kUN2koNtgNymv2ccKTR1PiX+obscJhsWzNbz3/Cjtr/IpEQRd\n"
-            + "E6qRptHDk0U2cHW4BYDSltndOktICdhWCWYLDxJHGjdyXqqqdEEFJ24u2fUJ3yF3\n"
-            + "NF2Bxa6llrmLb2fVeVYBzQSztQopKRWP9nt3ySoeJQqRWjNBN2j7cC93nrLHZTvB\n"
-            + "L/sWuTq5ecbXeeNVzxoBd21jmGrIUPNwGdDKdbTB0CjpLpVHOTwGByeRKQXhMlQB\n"
-            + "pK96wUpxxtShtOjNjN1s9GEyLHwDiHSuHNYs/AxxFzf9nbQhVGVzdHVzZXIgRm91\n"
-            + "ciA8dGVzdDRAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnU2cAhsDBgsJCAcDAgYV\n"
-            + "CAIJCgsEFgIDAQIeAQIXgAAKCRAIVSoXfKh4IXsHCACSm9RIdxxqibAaxh+nm6w5\n"
-            + "F5a6Hju5cdmkk9albDoQYh2eM8E5NdDq+r0qSSe2+ujDaQ4C95DZNJQESvIcHHHb\n"
-            + "9AECrBfS8Yk86rX8hxVeYQczMkB9LdBHximTSoOr8L/eAxBE/VXDwust6EAe6Q1A\n"
-            + "a3tlTTvCfcmw4PipvtP7F6UzFaq+QU6fvARpBATOcvVc2JU4JQOrxuNEQ2PKrSti\n"
-            + "75S5mnVWm0pRebM+EorWBtlA0eOAeLNqCp87UwLdvUyOTRZT4DJ51eTxfrFADXrI\n"
-            + "9/ejs3/YxCPYxaPicAlcldduuajU/s+9ifrUn0Npg2ILl8mQkNzqeerlBeecUV4E\n"
-            + "uQENBFWdTZwBCADEOsK+mFQ/2uds9znkmAqrk24waVBpyPGrTTXtXX0dKhtQAsh6\n"
-            + "QkZGkjLTnKxEsa9syqVckw+1JtCh44SP1gjqDUoShpBz5wIuksZ7q96Hx+F0TVG/\n"
-            + "njS6GrWvwKhL2Lb9hYfdlrZiYtOOi0iiOzud25H/Ms15kC8tuQm7NWtANJJF4Sxo\n"
-            + "Bxor6L/F4zunEkTL0L9/dp4qVrw23fJVKE38cSdxjB0u1qSDzLV/u0QJqlYxJAiE\n"
-            + "ciwQN2uVnTY1/XSpouMy6LvbYU7B2uU/WohNmH3RiN/fQ6jJm4x+fCZ8+zqXMiZn\n"
-            + "G2fPkwmxxK9cl64YnNGcTwsVt6BMbCHk9jHxABEBAAGJAR8EGAECAAkFAlWdTZwC\n"
-            + "GwwACgkQCFUqF3yoeCGOdwf/TmoxH3pFBm/MDhY5Ct5FO0KvsgQk2ZgDa68HyQ8j\n"
-            + "QYi1FUCtyDjsxf5KTfyvzpzcTpS7cyOwcJNtTj6UixwATkcivvYWYoOXghAsTo4f\n"
-            + "1+j/x6ECq1+nYE6NpcAN7VRJpYMk2UO2qlhHCesTPGzsHchL7mwiYdhGrdiWGTpd\n"
-            + "KI9WfOYDZZ9ZSw/QINJUyTRxrDnauOvVbhbAXc7jdKCkRQRZpsNlF//1Stg6nstj\n"
-            + "FJ7SrjVdsMJNlihT6fG5ujmrty1/6b1VCLkIQfW5cWvzRzTBFytq7i4PVKh3u7Oz\n"
-            + "tt9lf8s50zt2uBE/AKMkyE6IJLsBWpJPk7iFKkHGDx044Q==\n"
-            + "=477N\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP\n"
-            + "Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm\n"
-            + "qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y\n"
-            + "gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB\n"
-            + "7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR\n"
-            + "uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAEAB/4jqeZoOiACaV/Nygeh\n"
-            + "iOpJSiDsNDbrFRpKYdnhwT69APIQ2q5sshi+/dopbZVpkeBiIJk0UR7TAp3JVEPV\n"
-            + "rK92SMqjcCRYuMRkMeyZzMt7e4DjiN17ov6BSBjMZFSs4vnpTNKWk4ngHlaebe15\n"
-            + "6vq0sYK/XpKQxU7yAzQjxR190P/F+QEL98zVG/9uqM8PupfdSm4Smp2cIpfta+JD\n"
-            + "mO23HC6jAEm2RFwklovzgK3rbIjyiMuowIkAKx5xxRvpxMHf1l566b9zJrRi0xau\n"
-            + "vp4J/lnBJtTMzCbsaaFxhrj23xvTXaWR+UkaGPCv7wheXQ9K7NAHwmH8YrR+cZx7\n"
-            + "KbDlBADUTHZ+OhNslx/rkjRWrFuK9p49x7qxQc26kcqlGPbW6KOAMdUpwneQbhCG\n"
-            + "a36E/GAZgsgQ4SUqn37EVCtd2Y9Dp0inPAujcZXSwgDHev6ea7fzbxT9KLtEgvQN\n"
-            + "0vrFJDCPIt0wzGqNDw4wgFjF2rAafBO//Wu5K5QLW4hfzSguRQQA2u6DpVja/FYY\n"
-            + "UHVh2HLiB8th4T+qogOsBe5mKEsGRPXtAh7QzJu36C4PJyHeNlmlMx+15cCFnovj\n"
-            + "6cLpGn6ZP4okLyq2+VsW7wh/Vir+UZHoAO/cZRlOc1PsaQconcxxq30SsbaRQrAd\n"
-            + "YargKlXU7HMFiK34nkidBV6vVW0+P6cD/jYRInM983KXqX5bYvqsM1Zyvvlu6otD\n"
-            + "nG0F/nQYT7oaKKR46quDa+xHMxK8/Vu1+TabzY8XapnoYFaFvrl/d2rUBEZSoury\n"
-            + "z2yfTyeomft9MGGQsCGAJ95bVDT+jBoohnYwfwdC7HG3qk0aK/TxFyUqvMOX7SFe\n"
-            + "YT55n3HlD9InST+0IVRlc3R1c2VyIEZvdXIgPHRlc3Q0QGV4YW1wbGUuY29tPokB\n"
-            + "OAQTAQIAIgUCVZ1NnAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQCFUq\n"
-            + "F3yoeCF7BwgAkpvUSHccaomwGsYfp5usOReWuh47uXHZpJPWpWw6EGIdnjPBOTXQ\n"
-            + "6vq9Kkkntvrow2kOAveQ2TSUBEryHBxx2/QBAqwX0vGJPOq1/IcVXmEHMzJAfS3Q\n"
-            + "R8Ypk0qDq/C/3gMQRP1Vw8LrLehAHukNQGt7ZU07wn3JsOD4qb7T+xelMxWqvkFO\n"
-            + "n7wEaQQEznL1XNiVOCUDq8bjRENjyq0rYu+UuZp1VptKUXmzPhKK1gbZQNHjgHiz\n"
-            + "agqfO1MC3b1Mjk0WU+AyedXk8X6xQA16yPf3o7N/2MQj2MWj4nAJXJXXbrmo1P7P\n"
-            + "vYn61J9DaYNiC5fJkJDc6nnq5QXnnFFeBJ0DmARVnU2cAQgAxDrCvphUP9rnbPc5\n"
-            + "5JgKq5NuMGlQacjxq0017V19HSobUALIekJGRpIy05ysRLGvbMqlXJMPtSbQoeOE\n"
-            + "j9YI6g1KEoaQc+cCLpLGe6veh8fhdE1Rv540uhq1r8CoS9i2/YWH3Za2YmLTjotI\n"
-            + "ojs7nduR/zLNeZAvLbkJuzVrQDSSReEsaAcaK+i/xeM7pxJEy9C/f3aeKla8Nt3y\n"
-            + "VShN/HEncYwdLtakg8y1f7tECapWMSQIhHIsEDdrlZ02Nf10qaLjMui722FOwdrl\n"
-            + "P1qITZh90Yjf30OoyZuMfnwmfPs6lzImZxtnz5MJscSvXJeuGJzRnE8LFbegTGwh\n"
-            + "5PYx8QARAQABAAf8CeTumd6jbN7USXXDyQdzjkguR6mfwN29dcY8YF4U52oOm3+w\n"
-            + "bR23XmqTvoDJXONatZEYOm093wP4hBktP3Vq2KZX5Ew9r2JoBUIoWOcHHvCQqSUW\n"
-            + "6KMJBJNBMv3zXnOscmcPvTgStS5HfYn/XRLAhEqkd2ov2x/OiS8p0vM0F7YYSOdu\n"
-            + "X6/nHeBCM5QSJl00kgcaeQYdIGL0bPv9DnoeAC2/yITEvtvs+MHZ7FjH8A45QjWn\n"
-            + "DwfVoLg7WOc3wJtqJ55/r/2pylrWz0YYM8s6I3gbDilCF+Wb8tEIOaWJEwY73J1/\n"
-            + "KQG5qlO3/hBlO80DtzNmi3ylRUuzGhTxQfvemwQA3EuZ+E48LJ3dwtdJhh5mFlWI\n"
-            + "Ket21e5v1mqMxuLhf5/2CYcifM08u3EsEUdIr7egF25Sea8otqmCYcG8FuB37VY/\n"
-            + "Hd4G/+YVVaaAB8EU6u64YfSswhzr0R2qWVLtkJr0EAephzdPdoUEtKDSdTxnXiDV\n"
-            + "3vSqLWtZekScLa979uMEAOQIodJwxSvveKQWILjK67ZJr56X8YQZWA6rFsr1xMY0\n"
-            + "N0GH+5k0k+tr4wT3H9uk9ZM1Z11G3c01mhzCNg5roFoKtftKUZRKxmbfjjDmAofl\n"
-            + "bA6EZ0WHLdOwDLLTuXK09IsjjSHq0YHOxIlgFzIreuoxtz27bEEGhVFQg7xb0Lgb\n"
-            + "A/9LP8i32L7/CHsuN0q4YjhJkkaB6JWUQMFqWwoAXALG3rnw/CGRYHmHpiAuSeHR\n"
-            + "dSlZzndVi5poNC/d27msTx7ZuWlN7nOyywHBCTWV/nstm2I9rDhrHK7Axgq0Vv0y\n"
-            + "bWAurUmEgDJHU3ZpsNVt4e30FooXIDLR4cnpRM7tILv39D4giQEfBBgBAgAJBQJV\n"
-            + "nU2cAhsMAAoJEAhVKhd8qHghjncH/05qMR96RQZvzA4WOQreRTtCr7IEJNmYA2uv\n"
-            + "B8kPI0GItRVArcg47MX+Sk38r86c3E6Uu3MjsHCTbU4+lIscAE5HIr72FmKDl4IQ\n"
-            + "LE6OH9fo/8ehAqtfp2BOjaXADe1USaWDJNlDtqpYRwnrEzxs7B3IS+5sImHYRq3Y\n"
-            + "lhk6XSiPVnzmA2WfWUsP0CDSVMk0caw52rjr1W4WwF3O43SgpEUEWabDZRf/9UrY\n"
-            + "Op7LYxSe0q41XbDCTZYoU+nxubo5q7ctf+m9VQi5CEH1uXFr80c0wRcrau4uD1So\n"
-            + "d7uzs7bfZX/LOdM7drgRPwCjJMhOiCS7AVqST5O4hSpBxg8dOOE=\n"
-            + "=5aNq\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP
+        Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm
+        qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y
+        gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB
+        7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR
+        uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAGJAR8EIAECAAkFAlWdVXkC
+        HQIACgkQCFUqF3yoeCH4lgf/aBdTYqnwL1lreHbQaUXI0/B2zlMuoptoi/x+xjIB
+        7RszzaN3w0n4/87kUN2koNtgNymv2ccKTR1PiX+obscJhsWzNbz3/Cjtr/IpEQRd
+        E6qRptHDk0U2cHW4BYDSltndOktICdhWCWYLDxJHGjdyXqqqdEEFJ24u2fUJ3yF3
+        NF2Bxa6llrmLb2fVeVYBzQSztQopKRWP9nt3ySoeJQqRWjNBN2j7cC93nrLHZTvB
+        L/sWuTq5ecbXeeNVzxoBd21jmGrIUPNwGdDKdbTB0CjpLpVHOTwGByeRKQXhMlQB
+        pK96wUpxxtShtOjNjN1s9GEyLHwDiHSuHNYs/AxxFzf9nbQhVGVzdHVzZXIgRm91
+        ciA8dGVzdDRAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnU2cAhsDBgsJCAcDAgYV
+        CAIJCgsEFgIDAQIeAQIXgAAKCRAIVSoXfKh4IXsHCACSm9RIdxxqibAaxh+nm6w5
+        F5a6Hju5cdmkk9albDoQYh2eM8E5NdDq+r0qSSe2+ujDaQ4C95DZNJQESvIcHHHb
+        9AECrBfS8Yk86rX8hxVeYQczMkB9LdBHximTSoOr8L/eAxBE/VXDwust6EAe6Q1A
+        a3tlTTvCfcmw4PipvtP7F6UzFaq+QU6fvARpBATOcvVc2JU4JQOrxuNEQ2PKrSti
+        75S5mnVWm0pRebM+EorWBtlA0eOAeLNqCp87UwLdvUyOTRZT4DJ51eTxfrFADXrI
+        9/ejs3/YxCPYxaPicAlcldduuajU/s+9ifrUn0Npg2ILl8mQkNzqeerlBeecUV4E
+        uQENBFWdTZwBCADEOsK+mFQ/2uds9znkmAqrk24waVBpyPGrTTXtXX0dKhtQAsh6
+        QkZGkjLTnKxEsa9syqVckw+1JtCh44SP1gjqDUoShpBz5wIuksZ7q96Hx+F0TVG/
+        njS6GrWvwKhL2Lb9hYfdlrZiYtOOi0iiOzud25H/Ms15kC8tuQm7NWtANJJF4Sxo
+        Bxor6L/F4zunEkTL0L9/dp4qVrw23fJVKE38cSdxjB0u1qSDzLV/u0QJqlYxJAiE
+        ciwQN2uVnTY1/XSpouMy6LvbYU7B2uU/WohNmH3RiN/fQ6jJm4x+fCZ8+zqXMiZn
+        G2fPkwmxxK9cl64YnNGcTwsVt6BMbCHk9jHxABEBAAGJAR8EGAECAAkFAlWdTZwC
+        GwwACgkQCFUqF3yoeCGOdwf/TmoxH3pFBm/MDhY5Ct5FO0KvsgQk2ZgDa68HyQ8j
+        QYi1FUCtyDjsxf5KTfyvzpzcTpS7cyOwcJNtTj6UixwATkcivvYWYoOXghAsTo4f
+        1+j/x6ECq1+nYE6NpcAN7VRJpYMk2UO2qlhHCesTPGzsHchL7mwiYdhGrdiWGTpd
+        KI9WfOYDZZ9ZSw/QINJUyTRxrDnauOvVbhbAXc7jdKCkRQRZpsNlF//1Stg6nstj
+        FJ7SrjVdsMJNlihT6fG5ujmrty1/6b1VCLkIQfW5cWvzRzTBFytq7i4PVKh3u7Oz
+        tt9lf8s50zt2uBE/AKMkyE6IJLsBWpJPk7iFKkHGDx044Q==
+        =477N
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        lQOYBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP
+        Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm
+        qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y
+        gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB
+        7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR
+        uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAEAB/4jqeZoOiACaV/Nygeh
+        iOpJSiDsNDbrFRpKYdnhwT69APIQ2q5sshi+/dopbZVpkeBiIJk0UR7TAp3JVEPV
+        rK92SMqjcCRYuMRkMeyZzMt7e4DjiN17ov6BSBjMZFSs4vnpTNKWk4ngHlaebe15
+        6vq0sYK/XpKQxU7yAzQjxR190P/F+QEL98zVG/9uqM8PupfdSm4Smp2cIpfta+JD
+        mO23HC6jAEm2RFwklovzgK3rbIjyiMuowIkAKx5xxRvpxMHf1l566b9zJrRi0xau
+        vp4J/lnBJtTMzCbsaaFxhrj23xvTXaWR+UkaGPCv7wheXQ9K7NAHwmH8YrR+cZx7
+        KbDlBADUTHZ+OhNslx/rkjRWrFuK9p49x7qxQc26kcqlGPbW6KOAMdUpwneQbhCG
+        a36E/GAZgsgQ4SUqn37EVCtd2Y9Dp0inPAujcZXSwgDHev6ea7fzbxT9KLtEgvQN
+        0vrFJDCPIt0wzGqNDw4wgFjF2rAafBO//Wu5K5QLW4hfzSguRQQA2u6DpVja/FYY
+        UHVh2HLiB8th4T+qogOsBe5mKEsGRPXtAh7QzJu36C4PJyHeNlmlMx+15cCFnovj
+        6cLpGn6ZP4okLyq2+VsW7wh/Vir+UZHoAO/cZRlOc1PsaQconcxxq30SsbaRQrAd
+        YargKlXU7HMFiK34nkidBV6vVW0+P6cD/jYRInM983KXqX5bYvqsM1Zyvvlu6otD
+        nG0F/nQYT7oaKKR46quDa+xHMxK8/Vu1+TabzY8XapnoYFaFvrl/d2rUBEZSoury
+        z2yfTyeomft9MGGQsCGAJ95bVDT+jBoohnYwfwdC7HG3qk0aK/TxFyUqvMOX7SFe
+        YT55n3HlD9InST+0IVRlc3R1c2VyIEZvdXIgPHRlc3Q0QGV4YW1wbGUuY29tPokB
+        OAQTAQIAIgUCVZ1NnAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQCFUq
+        F3yoeCF7BwgAkpvUSHccaomwGsYfp5usOReWuh47uXHZpJPWpWw6EGIdnjPBOTXQ
+        6vq9Kkkntvrow2kOAveQ2TSUBEryHBxx2/QBAqwX0vGJPOq1/IcVXmEHMzJAfS3Q
+        R8Ypk0qDq/C/3gMQRP1Vw8LrLehAHukNQGt7ZU07wn3JsOD4qb7T+xelMxWqvkFO
+        n7wEaQQEznL1XNiVOCUDq8bjRENjyq0rYu+UuZp1VptKUXmzPhKK1gbZQNHjgHiz
+        agqfO1MC3b1Mjk0WU+AyedXk8X6xQA16yPf3o7N/2MQj2MWj4nAJXJXXbrmo1P7P
+        vYn61J9DaYNiC5fJkJDc6nnq5QXnnFFeBJ0DmARVnU2cAQgAxDrCvphUP9rnbPc5
+        5JgKq5NuMGlQacjxq0017V19HSobUALIekJGRpIy05ysRLGvbMqlXJMPtSbQoeOE
+        j9YI6g1KEoaQc+cCLpLGe6veh8fhdE1Rv540uhq1r8CoS9i2/YWH3Za2YmLTjotI
+        ojs7nduR/zLNeZAvLbkJuzVrQDSSReEsaAcaK+i/xeM7pxJEy9C/f3aeKla8Nt3y
+        VShN/HEncYwdLtakg8y1f7tECapWMSQIhHIsEDdrlZ02Nf10qaLjMui722FOwdrl
+        P1qITZh90Yjf30OoyZuMfnwmfPs6lzImZxtnz5MJscSvXJeuGJzRnE8LFbegTGwh
+        5PYx8QARAQABAAf8CeTumd6jbN7USXXDyQdzjkguR6mfwN29dcY8YF4U52oOm3+w
+        bR23XmqTvoDJXONatZEYOm093wP4hBktP3Vq2KZX5Ew9r2JoBUIoWOcHHvCQqSUW
+        6KMJBJNBMv3zXnOscmcPvTgStS5HfYn/XRLAhEqkd2ov2x/OiS8p0vM0F7YYSOdu
+        X6/nHeBCM5QSJl00kgcaeQYdIGL0bPv9DnoeAC2/yITEvtvs+MHZ7FjH8A45QjWn
+        DwfVoLg7WOc3wJtqJ55/r/2pylrWz0YYM8s6I3gbDilCF+Wb8tEIOaWJEwY73J1/
+        KQG5qlO3/hBlO80DtzNmi3ylRUuzGhTxQfvemwQA3EuZ+E48LJ3dwtdJhh5mFlWI
+        Ket21e5v1mqMxuLhf5/2CYcifM08u3EsEUdIr7egF25Sea8otqmCYcG8FuB37VY/
+        Hd4G/+YVVaaAB8EU6u64YfSswhzr0R2qWVLtkJr0EAephzdPdoUEtKDSdTxnXiDV
+        3vSqLWtZekScLa979uMEAOQIodJwxSvveKQWILjK67ZJr56X8YQZWA6rFsr1xMY0
+        N0GH+5k0k+tr4wT3H9uk9ZM1Z11G3c01mhzCNg5roFoKtftKUZRKxmbfjjDmAofl
+        bA6EZ0WHLdOwDLLTuXK09IsjjSHq0YHOxIlgFzIreuoxtz27bEEGhVFQg7xb0Lgb
+        A/9LP8i32L7/CHsuN0q4YjhJkkaB6JWUQMFqWwoAXALG3rnw/CGRYHmHpiAuSeHR
+        dSlZzndVi5poNC/d27msTx7ZuWlN7nOyywHBCTWV/nstm2I9rDhrHK7Axgq0Vv0y
+        bWAurUmEgDJHU3ZpsNVt4e30FooXIDLR4cnpRM7tILv39D4giQEfBBgBAgAJBQJV
+        nU2cAhsMAAoJEAhVKhd8qHghjncH/05qMR96RQZvzA4WOQreRTtCr7IEJNmYA2uv
+        B8kPI0GItRVArcg47MX+Sk38r86c3E6Uu3MjsHCTbU4+lIscAE5HIr72FmKDl4IQ
+        LE6OH9fo/8ehAqtfp2BOjaXADe1USaWDJNlDtqpYRwnrEzxs7B3IS+5sImHYRq3Y
+        lhk6XSiPVnzmA2WfWUsP0CDSVMk0caw52rjr1W4WwF3O43SgpEUEWabDZRf/9UrY
+        Op7LYxSe0q41XbDCTZYoU+nxubo5q7ctf+m9VQi5CEH1uXFr80c0wRcrau4uD1So
+        d7uzs7bfZX/LOdM7drgRPwCjJMhOiCS7AVqST5O4hSpBxg8dOOE=
+        =5aNq
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -446,100 +462,104 @@
    */
   public static TestKey validKeyWithSecondUserId() {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFW6jd4BCACrf+BZS3lntuWq2DWPOG07/BUWhx3RSoiS3JBuKEDmlsjswKcp\n"
-            + "JHT+p2tqH52XbujMlzNjAQcZjJwfOMt6Fg7zd3F8cwYQdCE/W5dpMs/mqdeEz6GL\n"
-            + "VJDZ0Y5wwz54ZQHp91Xq6uejxt5qffeTQk5cToQZ0RVx3iwBc+2P3iYJqMFmJzj8\n"
-            + "djabEoF4D50iI5tY8moE83VcXJ5Y4xn+5Z5AThmlfrMP6gIdG0b4lEe1tsnJC6AG\n"
-            + "GUU6VkzK6E1Tp93Y0brtWpJKi9Gt6eUqvWhZtPEdFVCFbLTpezUdRFEuaFbGg5pn\n"
-            + "9K/DceahFmquDJOHVgawt6erlq/ie7QEEld/ABEBAAG0IVRlc3R1c2VyIEZpdmUg\n"
-            + "PHRlc3Q1QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCVbqN3gIbAwYLCQgHAwIGFQgC\n"
-            + "CQoLBBYCAwECHgECF4AACgkQUCS7RJjFHb+/MAf9FKZatGcuOIoYqwGQQneyc63v\n"
-            + "3H/PyhvYF1nuKNftmhqIiUHec9RaUHQkgam6LRoonkDfIpNlQVRv2XBV2VOAOFVO\n"
-            + "RyQ/Tv7/xtpqGZqivV0yn2ZXbCceA627Vz7gP4gkO0ZJ0JsYJTc/5wO+nVG5Lohu\n"
-            + "/zdUofEbFAvcXs+Z1uXnUDdeGn47Lf1xZ2XOHOI0aQW4DdNaFoAd+AOTe0W3iB6W\n"
-            + "paCIGno69CyNHNnWjJCSD33oLVaXyvbgw5UoyITvSqRnPyLGIc6dsqDLT59ok0Fk\n"
-            + "t4jtiGu9aze4n59GbtSjmWQgzbLCQWhK9K7UCcSLYNKXVyMha2WapBO156V027QI\n"
-            + "Zm9vOm15SWSJATgEEwECACIFAlW6jwYCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B\n"
-            + "AheAAAoJEFAku0SYxR2/zZUH/1BwPsResHLDSmo6UdQyQGxvV0NcwBqGAPSLHr+S\n"
-            + "PHEaHEIYvOywNfWXquYrECa/5iIrXuTQmCH0q8WRcz1UapDCeD8Ui82r+3O8m6gk\n"
-            + "hIR5VAeza+x/fGWhG342PvtpDU7JycDA3KMCTWtcAM89tFhffzuEQ3f5p5cMTtZk\n"
-            + "/23iegXbHd61vojYO17QYEj+qp9l0VNiyFymPL3qr5bVj/xn/mXFj+asj0L2ypIj\n"
-            + "zC36FkhzW5EX2xgV9Cl9zu7kLMTm+yM+jxbMLskYkG8z/D+xBQsoX8tEIPlxHLhB\n"
-            + "miEmVuZrp91ArRMWa3B7PYz7hQzs+M/bxKXcmWxacggTOvy5AQ0EVbqN3gEIAOlq\n"
-            + "mwdiXW0BQP/iQvIweP1taNypAvdjI2fpnXkUfBT5X/+E/RjYOHQEAzy8nEkS+Y0l\n"
-            + "MLwKt3S0IVRvdeXxlpL6Tl+P8DkcD5H+uvACrg9rtgbbNSoQtc9/3bknG9hea6xi\n"
-            + "6SBH1k9Y2RInIrwWslfKmuNkyZVhxPKypasBsvyhOWLlpCngGiCa74KJ1th1WKa2\n"
-            + "aaDqcbieBTc1mtsXR6kBhJZqK+JYBoHriUQMs7nyXxn2qyv6Lehs/tHlrBZ7j16S\n"
-            + "faQzYoBi1edVrpFr/CuGk6RNKxG9vi/uAA9q2cLCMjjyfMH4g0G2l0HuDPQLA9wi\n"
-            + "BfusEC+OceaeFKtS9ykAEQEAAYkBHwQYAQIACQUCVbqN3gIbDAAKCRBQJLtEmMUd\n"
-            + "vw/DB/9Qx9m1eSdddqz/fk16wJf7Ncr2teVvdQOjRf/qo43KDKxEzeepjgypG1br\n"
-            + "St7U4/MlPygJLBDB4pXp0kaKt+S/aqLpEGSGzQ1FysM8oY6K0e1Kbf6nMaQS8ATG\n"
-            + "aD377FrUJ42NV4JS+NGlwaM9PhpRVm5n8iCzRs9HtlTyfCBkNGDjGOSdWcah2m6T\n"
-            + "fEQdD+XVDN1ZC8zAnc8FW28YOTeTjX079okP6ZCjLJ16VZ7eiHFkrNbS9Dl4SPNK\n"
-            + "eElvsZLBaf8t4RQXFFKwRq4BW+zS8zm9E2H6bZ9yGrmgIREzyRPpwU98g8yrabu0\n"
-            + "54w16Vp/SVViJs7nTMSug0WREyd2\n"
-            + "=ldwB\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFW6jd4BCACrf+BZS3lntuWq2DWPOG07/BUWhx3RSoiS3JBuKEDmlsjswKcp\n"
-            + "JHT+p2tqH52XbujMlzNjAQcZjJwfOMt6Fg7zd3F8cwYQdCE/W5dpMs/mqdeEz6GL\n"
-            + "VJDZ0Y5wwz54ZQHp91Xq6uejxt5qffeTQk5cToQZ0RVx3iwBc+2P3iYJqMFmJzj8\n"
-            + "djabEoF4D50iI5tY8moE83VcXJ5Y4xn+5Z5AThmlfrMP6gIdG0b4lEe1tsnJC6AG\n"
-            + "GUU6VkzK6E1Tp93Y0brtWpJKi9Gt6eUqvWhZtPEdFVCFbLTpezUdRFEuaFbGg5pn\n"
-            + "9K/DceahFmquDJOHVgawt6erlq/ie7QEEld/ABEBAAEAB/9MIlrQiWb+Gf3fWFh+\n"
-            + "mkg0Bva9p4IfNX1n5S7hGFGnjGzqXaRX6W1e16gh1qM5ZO1IVh9j5kLmnrt4SNhb\n"
-            + "/Irqnq3s14trpoJUBC81bm9JMUESHrLSjdo4OIWJncOP4xd0bG7h+SKYXGLE1+Me\n"
-            + "pqLu65RNebqRcFYM1xAxfCdaxatcz+LrW5ZX+6T/Gh/VCHRkkzzVIZO1dDBbyU2C\n"
-            + "JrNcfHSvNrjzfqYHtwfsk/lwcuY9pqkYcuwZ2IM+iWKit+WyCR2BzOpG/Sva1t8b\n"
-            + "7B7ituQCFMCv5IiaAoaSKX/t/0ucWCoT1ttih8LdwgEE0kgij/ZUfRxCiL9HmtLy\n"
-            + "ad9BBADBGYWv6NiTQiBG7+MZ+twCjlSL7vq8iENhQYZShGHF9z+ju7m8U1dteLny\n"
-            + "pC3NcNfCgWyy+8lRn1e6Oe6m7xL83LL3HJT5nIy9mpsCw/TIrrkzkoE+VpkEIL/o\n"
-            + "Yeoxauah4SU7laVD29aAQZ3TqwSwx0sJwPjsj73WjjqtzJfFkQQA410ghqMbQZN1\n"
-            + "yJzXgVAj162ZwTi961N5iYmqTiBtqGz1UfaNBJWdJMkCmhMTsiOtm1h4zUQRuEH+\n"
-            + "yq1xhKOGf15dB/cLSMj2KpVVlvgLoVmYDugSER8Q23juilY7iaf0bqo9q1sTHpn9\n"
-            + "O7Oin/9J3sz+ic45vDh4aa74sOzfhA8EAJwAFEWLrGSxtnYJR5vQNstHIH1wtQ5G\n"
-            + "ZUZ57y9CbDkKrfCQvd0JOBjfUDz+N8qiamNIqfhQBtlhIDYgtswiG+iGP/2G0l6S\n"
-            + "j9DHNe2CYPUKgy+zQiRnyNGE2XUfcE+HuNDfu3AryPqaD8vLLw8TnsAgis3bRGg+\n"
-            + "hhrAC1NyKfDXTg20IVRlc3R1c2VyIEZpdmUgPHRlc3Q1QGV4YW1wbGUuY29tPokB\n"
-            + "OAQTAQIAIgUCVbqN3gIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQUCS7\n"
-            + "RJjFHb+/MAf9FKZatGcuOIoYqwGQQneyc63v3H/PyhvYF1nuKNftmhqIiUHec9Ra\n"
-            + "UHQkgam6LRoonkDfIpNlQVRv2XBV2VOAOFVORyQ/Tv7/xtpqGZqivV0yn2ZXbCce\n"
-            + "A627Vz7gP4gkO0ZJ0JsYJTc/5wO+nVG5Lohu/zdUofEbFAvcXs+Z1uXnUDdeGn47\n"
-            + "Lf1xZ2XOHOI0aQW4DdNaFoAd+AOTe0W3iB6WpaCIGno69CyNHNnWjJCSD33oLVaX\n"
-            + "yvbgw5UoyITvSqRnPyLGIc6dsqDLT59ok0Fkt4jtiGu9aze4n59GbtSjmWQgzbLC\n"
-            + "QWhK9K7UCcSLYNKXVyMha2WapBO156V0250DmARVuo3eAQgA6WqbB2JdbQFA/+JC\n"
-            + "8jB4/W1o3KkC92MjZ+mdeRR8FPlf/4T9GNg4dAQDPLycSRL5jSUwvAq3dLQhVG91\n"
-            + "5fGWkvpOX4/wORwPkf668AKuD2u2Bts1KhC1z3/duScb2F5rrGLpIEfWT1jZEici\n"
-            + "vBayV8qa42TJlWHE8rKlqwGy/KE5YuWkKeAaIJrvgonW2HVYprZpoOpxuJ4FNzWa\n"
-            + "2xdHqQGElmor4lgGgeuJRAyzufJfGfarK/ot6Gz+0eWsFnuPXpJ9pDNigGLV51Wu\n"
-            + "kWv8K4aTpE0rEb2+L+4AD2rZwsIyOPJ8wfiDQbaXQe4M9AsD3CIF+6wQL45x5p4U\n"
-            + "q1L3KQARAQABAAf8C+2DsJPpPEnFHY5dZ2zssd6mbihA2414YLYCcw6F7Lh1nGQa\n"
-            + "XuulruAJnk/xGJbco8bTv7g4ecE+tsbfWnnG/QnHeYCsgO6bKRXATcWFSYpyidUn\n"
-            + "2VdzQwBAv1ZtSNhCXlPLn/erzvA2X4QadUwfnvbehWJAHt8ZJmHUr3FtyRUHEdCK\n"
-            + "2EXsBWnzPCcqHZOMvcbSINSqBFGzVXkOZsMFvPTNIUYRHz8NbJT/OPiOmyBshXpS\n"
-            + "t8w3QqZhBcTT3NZo3kgxN1RygaTa10ytB2cxTCVuD8hmUBaV9gakdfMYkVJds7/T\n"
-            + "ZY3It68F0vitBnqpppZQ+NFgr/vwVg0p3gbmAQQA79zsWPvyIqYvyJhmiKvLIpev\n"
-            + "569ho8tC9xx+IZ5WnjN8ZADlb9brAdA9cqGfBgZkpZUhngCRVOYUIco+m2NYkEJm\n"
-            + "BsSTTM77dqU55DRloJ3FtBwCPXHkwg9P/FHMMYYGyLpQTSB92hXk8yomo+ozX7kx\n"
-            + "DtUHZIrir/rr0lQe+GkEAPkep9V5jBmfHMArnfji7Nfb1/ZjrSAaK+rtqczgm+6j\n"
-            + "ubY/0DpM/6gm+/8X27WFw2m45ncH3qNvOe4Qm40EmgmHkXsdQyU0Fv7uXc9nBYoo\n"
-            + "G6s7DWLY4VAqWwPsvbqgpSp/qdGn9nlcJjjY1HtfU7HM3xysT7TJ2YVhYHlJdjDB\n"
-            + "A/0alBcYtHvaCJaRLWX4UiashbfETWAf/4oHlERjkXj64qOdsGnD6CD99t9x91Ue\n"
-            + "pClPsLDFvY8/HxWX7STA9pQZAa2ZdJd8b58Rgy9TBShw2mbz2S6Cbw77pP/WEjtJ\n"
-            + "pJuS2gDp70H01fYRaw7YH32CfUr1VeEv7hTjk/SNVteIZkkOiQEfBBgBAgAJBQJV\n"
-            + "uo3eAhsMAAoJEFAku0SYxR2/D8MH/1DH2bV5J112rP9+TXrAl/s1yva15W91A6NF\n"
-            + "/+qjjcoMrETN56mODKkbVutK3tTj8yU/KAksEMHilenSRoq35L9qoukQZIbNDUXK\n"
-            + "wzyhjorR7Upt/qcxpBLwBMZoPfvsWtQnjY1XglL40aXBoz0+GlFWbmfyILNGz0e2\n"
-            + "VPJ8IGQ0YOMY5J1ZxqHabpN8RB0P5dUM3VkLzMCdzwVbbxg5N5ONfTv2iQ/pkKMs\n"
-            + "nXpVnt6IcWSs1tL0OXhI80p4SW+xksFp/y3hFBcUUrBGrgFb7NLzOb0TYfptn3Ia\n"
-            + "uaAhETPJE+nBT3yDzKtpu7TnjDXpWn9JVWImzudMxK6DRZETJ3Y=\n"
-            + "=uND5\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        54w16Vp/SVViJs7nTMSug0WREyd2
+        =ldwB
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =uND5
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -555,113 +575,117 @@
    */
   public static TestKey revokedCompromisedKey() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFYmpXkBCACqaLz51DcWQmfOJnat9iHSySfSHwbKfVvoN43Ba2cf/D/PadRc\n"
-            + "HLgc+91k2yk1kV1LnMdvUGj5zZ84ZqrQx3f1WeItnzZpqxtmQS/GSxCp9EY/s7w6\n"
-            + "5i86R/k9Tzgvk0B7dKZJXbM/OWxxDkkxHWE3Un9wreX7bDU5b9D2knHRiNFqH9ZJ\n"
-            + "KqDIFZqH9WTUxNZcHz20sTCRIMfvsAwf2vRU5N5xTu4Mbk6JFc7BAj7h1f/mYEPo\n"
-            + "CTyB1jV/DSDVdn1FjJVocSg6W/CvsYF9hKFYjJHl4VXdePTpnOjHhJLL0QWk0TMe\n"
-            + "xYeUi/xDr5DeMxTmi7F7BFaQEF+KmUM46e+9ABEBAAGJATAEIAECABoFAlYmq1gT\n"
-            + "HQJ0ZXN0NiBjb21wcm9taXNlZAAKCRDtBiXcRjKKjIm6B/9YwkyG4w+9KUNESywM\n"
-            + "bxC2WWGWrFcQGoKxixzt0uT251UY8qxa1IED0wnLsIQmffTQcnrK3B9svd4HhQlk\n"
-            + "pheKQ3w5iluLeGmGljhDBdAVyS07jYoFUGTXjwzPAgJ3Dxzul8Q8Zj+fOmRcfsP9\n"
-            + "72kl6g2yEEbevnydWIiOj/vWHVLFb54G8bwXTNwH/FXQsHuPYxXZifwyDwdwEQMq\n"
-            + "0VTZcrukgeJ+VbSSuq+uX4I3+kJw5hL49KYAQltQBmTo3yhuY/Q+LkgcBv/umtY/\n"
-            + "DrUqSCBV1bTnfq5SfaObkUu22HWjrtSFSjnXYyh+wyTG3AXG3N9VPrjGQIJIW1j6\n"
-            + "9QM0iQE3BB8BAgAhBQJWJqYUFwyAAQSup+0vghEz5bEo0e0GJdxGMoqMAgcAAAoJ\n"
-            + "EIxP0o40NLOfYd4H/3GpfxfJ+nMzBChn1JqFrKOqqYiOO4sUwubXzpRO33V2jUrU\n"
-            + "V75PTWG/6NlgDbPfKFcU0qZud6M2EQxSS9/I20i/MpRB7qJnWMM/6HxdMDJ0o/pN\n"
-            + "4ImIGj38QTIWx0DS9n3bwlcobl7ZlM8g2N1kv5jQPEuurffeJRS4ny4pEvCCm2IS\n"
-            + "SGOuB0DVtYHGDrJLQ0k4mDkEJuU8fP5un8mN8I8eAINlsTFpsTswMXMiptZTm5SI\n"
-            + "5QZlG3m5MvvckngYdhynvCWc6JHGt1EHXlI4A5Qetr/4FbNE4uYcEEhyzBy4WQfi\n"
-            + "QCPiIzzm3O4cMnr9N+5HzYqRhu2OveYm86G2Rxq0IFRlc3R1c2VyIFNpeCA8dGVz\n"
-            + "dDZAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJWJqV5AhsDBgsJCAcDAgYVCAIJCgsE\n"
-            + "FgIDAQIeAQIXgAAKCRCMT9KONDSzn2XtB/4wl4ctc3cW9Fwp17cktFi6md8fjRiR\n"
-            + "wE/ruVKIKmAHzeMLBoZn4LZVunyNCRGLZfP+MUs4JhLkp8ioTzUB7xPl9k94FXel\n"
-            + "bObn9F0T7htjFLiFAOMeykneylk2kalTt6IBKtaOPn+V6onBwO+YHbwt+xLMhAWj\n"
-            + "Z/WA0TIC1RIukdzWErhd+9lG8B9kupGC5bPo/AgCPoajPhS1qLrth+lCsNJXT/Rt\n"
-            + "k6Jx5omypxMXPzgzNtULMFONszaRnHnrCHQg/yJZDCw3ffW5ShfyfWdFM65jgEKo\n"
-            + "nMKLzy9XV+BM6IJQlgHCBAP8WHKSf4qMG4/hEWLrwA/bTQ7w0DSV88msuQENBFYm\n"
-            + "pXkBCACzIMFDC6kcV58uvF3XwOrS3DmKNPDNzO/4Ay/iOxZbm+9NP8QWEEm+AzCt\n"
-            + "ZMfYdZ8C3DjuzxkhcacI/E5agZICds6bs0+VS7VKEeNYp/UrTF9pkZNXseCrJPgr\n"
-            + "U31eoGVc5bE5c0TGLhAjbMKtR5LZFMpAXgpA7hXJSSuAXGs8gjkJkYSJYnJwIOyd\n"
-            + "xOi5jmnE/U5QuMjBG0bwxFXxkaDa5mcebJ/6C8mgkKyATbQkCe7YJGl1JLK4vY28\n"
-            + "ybSMhMDtZiwgvKzd+HcQr+xUQvmgSMApJaMxKPHRA1IrP/STXUEAjcGfk/HCz/0j\n"
-            + "7mJG2cvCxeOMAmp/pTzhSoXiqUNlABEBAAGJAR8EGAECAAkFAlYmpXkCGwwACgkQ\n"
-            + "jE/SjjQ0s5/kVAf/QvHOhuoBSlSxPcgvnvCl8V3zbNR1P9lgjYGwMsvLhwCT7Wvm\n"
-            + "mkUKvtT913uER93N8xJD2svGhKabpiPj9/eo0p3p64dicijsP1UQfpmWKPa/V9sv\n"
-            + "zep08cpDl/eczSiLqgcTXCoZeewWXoQGqqoXnwa4lwQv4Zvj7TTCN2wRzoGwbRcm\n"
-            + "G2hmc27uOwA+hXbF+bLe6HOZR/7U93j8a22g2X9OgST/QCsLgyiUSw3YYaEan9tn\n"
-            + "wuEgAEY/rchOvgeXe5Sl0wTFLHH6OS4BBGgc1LRKnSCM2dgZqvhOOxOvuuieBWY6\n"
-            + "tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==\n"
-            + "=Dxr7\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFYmpXkBCACqaLz51DcWQmfOJnat9iHSySfSHwbKfVvoN43Ba2cf/D/PadRc\n"
-            + "HLgc+91k2yk1kV1LnMdvUGj5zZ84ZqrQx3f1WeItnzZpqxtmQS/GSxCp9EY/s7w6\n"
-            + "5i86R/k9Tzgvk0B7dKZJXbM/OWxxDkkxHWE3Un9wreX7bDU5b9D2knHRiNFqH9ZJ\n"
-            + "KqDIFZqH9WTUxNZcHz20sTCRIMfvsAwf2vRU5N5xTu4Mbk6JFc7BAj7h1f/mYEPo\n"
-            + "CTyB1jV/DSDVdn1FjJVocSg6W/CvsYF9hKFYjJHl4VXdePTpnOjHhJLL0QWk0TMe\n"
-            + "xYeUi/xDr5DeMxTmi7F7BFaQEF+KmUM46e+9ABEBAAEAB/wOspbuA1A3AsY6QRYG\n"
-            + "Xg6/w+rD1Do9N7+4ESaQUqej2hlU1d9jjHSSx2RqgP6WaLG/xkdrQeez9/iuICjG\n"
-            + "dhXSGw0He05xobjswl2RAENxLSjr8KAhAl57a97C23TQoaYzn7WB6Wt+3gCM5bsJ\n"
-            + "WevbHinwuYb2/ve+OvcudSYM+Nhtpv0DoTaizhi9wzc3g/XLbturlpdCffbw4y+h\n"
-            + "gBPd/t3cc/0Ams8Wi2RlmDOoe73ls23nBHcNomgydyIYBn7U5Z3v3YkPNp9VBiXx\n"
-            + "rC4mDtB1ugucMhqjRNAYqinaLP35CiBTU/IB0WLu7ZyytnjY5frly1ShAG8wFL0B\n"
-            + "MOMxBADJjGy1NwGSd/7eMeYyYThyhXDxo5so91/O1+RLnSUVv/Nz6VOPp2TtuVN5\n"
-            + "uTJkpSXtUFyWbf8mkQiFz4++vHW5E/Q6+KomXRalK7JeBzeFMtax64ykQHID9cSu\n"
-            + "TaSHBhOEEeZZuf6BlulYEJEBHYK6EFlPJn+cpZtTFaqDoKh22QQA2HKjfyeppNre\n"
-            + "WRFJ9h1x1hBlSRR+XIPYmDmZUjL37jQUlw8iF+txPclfyNBw2I2Om+Jhcf25peOx\n"
-            + "ow4yvjt8r3qDjNhI2zLE9u4zrQ9xU8CUingT0t4k3NO2vigpKlmp1/w2IHSMctry\n"
-            + "v1v3+BAS8qGIYDY1lgI7QBvle5hxGYUD/00zMyHOIgYg/cM5sR0qafesoj9kRff5\n"
-            + "UMnSy1dw+pGMv6GqKGbcZDoC060hUO9GhQRPZXF8PlYzD30lOLS2Uw4mPXjOmQVv\n"
-            + "lDiyl/vLkfkVfP/alYH0FW6mErDrjtHhrZewqDm3iPLGMVGfGCJsL+N37VBSe+jr\n"
-            + "4rZCnjk/Jo5JRoKJATcEHwECACEFAlYmphQXDIABBK6n7S+CETPlsSjR7QYl3EYy\n"
-            + "iowCBwAACgkQjE/SjjQ0s59h3gf/cal/F8n6czMEKGfUmoWso6qpiI47ixTC5tfO\n"
-            + "lE7fdXaNStRXvk9NYb/o2WANs98oVxTSpm53ozYRDFJL38jbSL8ylEHuomdYwz/o\n"
-            + "fF0wMnSj+k3giYgaPfxBMhbHQNL2fdvCVyhuXtmUzyDY3WS/mNA8S66t994lFLif\n"
-            + "LikS8IKbYhJIY64HQNW1gcYOsktDSTiYOQQm5Tx8/m6fyY3wjx4Ag2WxMWmxOzAx\n"
-            + "cyKm1lOblIjlBmUbebky+9ySeBh2HKe8JZzokca3UQdeUjgDlB62v/gVs0Ti5hwQ\n"
-            + "SHLMHLhZB+JAI+IjPObc7hwyev037kfNipGG7Y695ibzobZHGrQgVGVzdHVzZXIg\n"
-            + "U2l4IDx0ZXN0NkBleGFtcGxlLmNvbT6JATgEEwECACIFAlYmpXkCGwMGCwkIBwMC\n"
-            + "BhUIAgkKCwQWAgMBAh4BAheAAAoJEIxP0o40NLOfZe0H/jCXhy1zdxb0XCnXtyS0\n"
-            + "WLqZ3x+NGJHAT+u5UogqYAfN4wsGhmfgtlW6fI0JEYtl8/4xSzgmEuSnyKhPNQHv\n"
-            + "E+X2T3gVd6Vs5uf0XRPuG2MUuIUA4x7KSd7KWTaRqVO3ogEq1o4+f5XqicHA75gd\n"
-            + "vC37EsyEBaNn9YDRMgLVEi6R3NYSuF372UbwH2S6kYLls+j8CAI+hqM+FLWouu2H\n"
-            + "6UKw0ldP9G2TonHmibKnExc/ODM21QswU42zNpGceesIdCD/IlkMLDd99blKF/J9\n"
-            + "Z0UzrmOAQqicwovPL1dX4EzoglCWAcIEA/xYcpJ/iowbj+ERYuvAD9tNDvDQNJXz\n"
-            + "yaydA5gEVialeQEIALMgwUMLqRxXny68XdfA6tLcOYo08M3M7/gDL+I7Flub700/\n"
-            + "xBYQSb4DMK1kx9h1nwLcOO7PGSFxpwj8TlqBkgJ2zpuzT5VLtUoR41in9StMX2mR\n"
-            + "k1ex4Ksk+CtTfV6gZVzlsTlzRMYuECNswq1HktkUykBeCkDuFclJK4BcazyCOQmR\n"
-            + "hIlicnAg7J3E6LmOacT9TlC4yMEbRvDEVfGRoNrmZx5sn/oLyaCQrIBNtCQJ7tgk\n"
-            + "aXUksri9jbzJtIyEwO1mLCC8rN34dxCv7FRC+aBIwCklozEo8dEDUis/9JNdQQCN\n"
-            + "wZ+T8cLP/SPuYkbZy8LF44wCan+lPOFKheKpQ2UAEQEAAQAH/A1Os+Tb9yiGnuoN\n"
-            + "LuiSKa/YEgNBOxmC7dnuPK6xJpBQNZc200WzWJMf8AwVpl4foNxIyYb+Rjbsl1Ts\n"
-            + "z5JcOWFq+57oE5O7D+EMkqf5tFZO4nC4kqprac41HSW02mW/A0DDRKcIt/WEIwlK\n"
-            + "sWzHmjJ736moAtl/holRYQS0ePgB8bUPDQcFovH6X3SUxlPGTYD1DEX+WNvYRk3r\n"
-            + "pa9YXH65qbG9CEJIFTmwZIRDl+CBtBlN/fKadyMJr9fXtv7Fu9hNsK1K1pUtLqCa\n"
-            + "nc22Zak+o+LCPlZ8vmw/UmOGtp2iZlEragmh2rOywp0dHF7gsdlgoafQf8Q4NIag\n"
-            + "TFyHf1kEAMSOKUUwLBEmPnDVfoEOt5spQLVtlF8sh/Okk9zVazWmw0n/b1Ef72z6\n"
-            + "EZqCW9/XhH5pXfKJeV+08hroHI6a5UESa7/xOIx50TaQdRqjwGciMnH2LJcpIU/L\n"
-            + "f0cGXcnTLKt4Z2GeSPKFTj4VzwmwH5F/RYdc5eiVb7VNoy9DC5RZBADpTVH5pklS\n"
-            + "44VDJIcwSNy1LBEU3oj+Nu+sufCimJ5B7HLokoJtm6q8VQRga5hN1TZkdQcLy+b2\n"
-            + "wzxHYoIsIsYFfG/mqLZ3LJNDFqze1/Kj987DYSUGeNYexMN2Fkzbo35Jf0cpOiao\n"
-            + "390JFOS7qecUak5/yJ/V4xy8/nds37617QP9GWlFBykDoESBC2AIz8wXcpUBVNeH\n"
-            + "BNSthmC+PJPhsS6jTQuipqtXUZBgZBrMHp/bA8gTOkI4rPXycH3+ACbuQMAjbFny\n"
-            + "Kt69lPHD8VWw/82E4EY2J9LmHli+2BcATz89ouC4kqC5zF90qJseviSZPihpnFxA\n"
-            + "1UqMU2ZjsPb4CM9C/YkBHwQYAQIACQUCVialeQIbDAAKCRCMT9KONDSzn+RUB/9C\n"
-            + "8c6G6gFKVLE9yC+e8KXxXfNs1HU/2WCNgbAyy8uHAJPta+aaRQq+1P3Xe4RH3c3z\n"
-            + "EkPay8aEppumI+P396jSnenrh2JyKOw/VRB+mZYo9r9X2y/N6nTxykOX95zNKIuq\n"
-            + "BxNcKhl57BZehAaqqhefBriXBC/hm+PtNMI3bBHOgbBtFyYbaGZzbu47AD6FdsX5\n"
-            + "st7oc5lH/tT3ePxrbaDZf06BJP9AKwuDKJRLDdhhoRqf22fC4SAARj+tyE6+B5d7\n"
-            + "lKXTBMUscfo5LgEEaBzUtEqdIIzZ2Bmq+E47E6+66J4FZjq1Qu8gQiM00/xCLN9z\n"
-            + "i7Y7yHsc/ZvfJhKun0wx\n"
-            + "=M/kw\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==
+        =Dxr7
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        i7Y7yHsc/ZvfJhKun0wx
+        =M/kw
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -677,113 +701,117 @@
    */
   public static TestKey revokedNoLongerUsedKey() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBFYmq3EBCAC9ssY6QhFsnZqKEPlQrx8Zomblj8qV93/B448isOT2L6OVY7UC\n"
-            + "kKPj6afW5UDkYeyZSmLZfTrpePcbAB8FB3uvd/AS9mHC+6zuBlwlkl9xIXlwUXQP\n"
-            + "KER4LKYNTP21AM+/vTJm4+u26tlZECIZlez31KEeqM30EAm+/pO8VkEp8+1ImfLv\n"
-            + "otndIjMoq9gxJvn6KZeexJT2eKCsSa20vVsmAhuFjLZitU3lEjIROfDiyHUZ2cZ+\n"
-            + "qynfppJCKlHJRu/T9L/yxDFVUFDFSajNzSfjG1g3FEveDITyAhRetVfZbhyJptnV\n"
-            + "jfiHSQkLamPsBmMoKfP+aO5SfsTHTJvxgLUdABEBAAGJAS0EIAECABcFAlYmq8AQ\n"
-            + "HQN0ZXN0NyBub3QgdXNlZAAKCRDtBiXcRjKKjPKqB/sF+ypJZaZ5M4jFdoH/YA3s\n"
-            + "4+VkA/NbLKcrlMI0lbnIrax02jdyTo7rBUJfTwuBs5QeQ25+VfaBcz9fWSv4Z8Bk\n"
-            + "9+w61bQZLQkExZ9W7hnhaapyR0aT0rY48KGtHOPNoMQu9Si+RnRiI024jMUUjrau\n"
-            + "w/exgCteY261VtCPRgyZOlpbX43rsBhF8ott0ZzSfLwaNTHhsjFsD1uH6TSFO8La\n"
-            + "/H1nO31sORlY3+rCGiQVuYIJD1qI7bEjDHYO0nq/f7JjfYKmVBg9grwLsX3h1qZ2\n"
-            + "L3Yz+0eCi7/6T/Sm7PavQ+EGL7+WBXX3qJpwc+EFNHs6VxQp86k6csba0c5mNcaQ\n"
-            + "iQE3BB8BAgAhBQJWJqusFwyAAQSup+0vghEz5bEo0e0GJdxGMoqMAgcAAAoJEPrS\n"
-            + "q+I9bFLQ2BYH/jm+t7pZuv8WqZdb8FiBa9CFfhcSKjYarMHjBw7GxWZJMd5VR4DC\n"
-            + "r4T/ZSAGRKBRKQ2uXrkm9H0NPDp0c/UKCHtQMFDnqTk7B63mwSR1d7W0qaRPXYQ1\n"
-            + "bbatnzkEDOj0e+rX6aiqVRMo/q6uMNUFl6UMrUZPSNB5PVRQWPnQ7K11mw3vg0e5\n"
-            + "ycqJbyFvER6EtyDUXGBo8a5/4bK8VBNBMTAIy6GeGpeSM5b7cpQk7/j4dXugCJAV\n"
-            + "fhFNUOgLduoIKM4u+VcFjk3Km/YxOtGi1dLqCbTX/0LiCRA9mgQpyNVyA+Sm48LM\n"
-            + "LUkbcrN/F3SHX1ao/5lm19r8Biu1ziQnLgC0IlRlc3R1c2VyIFNldmVuIDx0ZXN0\n"
-            + "N0BleGFtcGxlLmNvbT6JATgEEwECACIFAlYmq3ECGwMGCwkIBwMCBhUIAgkKCwQW\n"
-            + "AgMBAh4BAheAAAoJEPrSq+I9bFLQvjQH/0K7aBsGU2U/rm4I+u+uPa6BnFTYQJqg\n"
-            + "034pwdD0WfM3M/XgVh7ERjnR9ZViCMVej+K3kW5d2DNaXu5vVpcD6L6jjWwiJHBw\n"
-            + "LIcmpqQrL0TdoCr4F4FKQnBbcH1fNvP8A/hLDHB3k3ERPvEFIo1AkVuK4s/v7yZY\n"
-            + "HAowX0r4ok4ndu/wAc0HI1FkApkAfh18JDTuui53dkKhnkDp7Xnfm/ElAZYjB7Se\n"
-            + "ivxOD9vdhViWSx1VhttPZo5hSyJrEYaJ5u9hsXNUN85DxgLqCmS1v8n3pN1lVY/Q\n"
-            + "TYXtgocakQgHGEG0Tl6a3xpNkn9ihnyCr80mHCxXTyUUBGfygccelB+5AQ0EViar\n"
-            + "cQEIAKxwXb6HGV9QjepADyWW7GMxc2JVZ7pZM2sdf8wrgnQqV2G1rc9gAgwTX4jt\n"
-            + "OY0vSKT1vBq09ZXS3qpYHi/Wwft0KkaX/a7e6vKabDSfhilxC2LuGz2+56f6UOzj\n"
-            + "ggwf5k4LFTQvkDUZumwPjoeC2hqQO3Q/9PW39C6GnvsCr5L0MRdO3PbVJM7lJaOk\n"
-            + "MbGwgysErWgiZXKlxMpIvffIsLC4BAxnjXaCy6zHuBcPMPaRMs7sDRBzeuTV2wnX\n"
-            + "Sd+IXZgdpd1hF7VkuXenzwOqvBGS66C3ILW0ZTFaOtgrloIkTvtYEcJFWvxqWl2F\n"
-            + "+JQ5V6eu2aJ3HIGyr9L1R8MUA6EAEQEAAYkBHwQYAQIACQUCViarcQIbDAAKCRD6\n"
-            + "0qviPWxS0M0PB/9Rbk4/pNW67+uE1lwtaIG7uFiMbJqu8jK6MkD8GdayflroWEZA\n"
-            + "x0Xow9HL8UaRfeRPTZMrDRpjl+fJIXT5qnlB0FPmzSXAKr3piC8migBcbp5m6hWh\n"
-            + "c3ScAqWOeMt9j0TTWHh4hKS8Q+lK392ht65cI/kpFhxm9EEaXmajplNL/2G3PVrl\n"
-            + "fFUgCdOn2DYdVSgJsfBhkcoiy17G3vqtb+We6ulhziae4SIrkUSqdYmRjiFyvqZz\n"
-            + "tmMEoF6CQNCUb1NK0TsSDeIdDacYjUwyq0Qj6TaXrWcbC3kW0GtWoFTNIiX4q9bN\n"
-            + "+B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC\n"
-            + "=CHer\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBFYmq3EBCAC9ssY6QhFsnZqKEPlQrx8Zomblj8qV93/B448isOT2L6OVY7UC\n"
-            + "kKPj6afW5UDkYeyZSmLZfTrpePcbAB8FB3uvd/AS9mHC+6zuBlwlkl9xIXlwUXQP\n"
-            + "KER4LKYNTP21AM+/vTJm4+u26tlZECIZlez31KEeqM30EAm+/pO8VkEp8+1ImfLv\n"
-            + "otndIjMoq9gxJvn6KZeexJT2eKCsSa20vVsmAhuFjLZitU3lEjIROfDiyHUZ2cZ+\n"
-            + "qynfppJCKlHJRu/T9L/yxDFVUFDFSajNzSfjG1g3FEveDITyAhRetVfZbhyJptnV\n"
-            + "jfiHSQkLamPsBmMoKfP+aO5SfsTHTJvxgLUdABEBAAEAB/9AdCtFJSidcolNKwpC\n"
-            + "/1V+VL9IdYxcWx02CDccjuUkvrgCrL+WcQW2jS/hZMChOKJ2zR78DcBEDr1LF8Xy\n"
-            + "ZAIC8yoHj15VLUUrFM8fVvYFzt1fq9VWxxRIjscW0teLNgzgdYzYB84RtwcFa2Vi\n"
-            + "sx2ycTUTYUClEgP1uLMCtX3rnibJh4vR+lVgnDtKSoh4CLAlW6grAAVdw5sSuV7Q\n"
-            + "i9EJcPezGw1RvBU5PooqNDG6kyw/QqsAS4q3WP4uVJKK1e7S9oqXFEN8k/zfllI0\n"
-            + "SSkoyP2flzz71rJF/wQMfJ8uf/CelKXd+gPO4FbCWiZSTLe20JR23qiOyvZkfCwg\n"
-            + "eFmzBADIJUzspDrg5yaqE+HMc8U3O9G9FHoDSweZTbhiq3aK0BqMAn34u0ps6chy\n"
-            + "VMO6aPWVzgcSHNfTlzpjuN9lwDoimYBH5vZa1HlCHt5eeqTORixkxSerOmILabTi\n"
-            + "QWq5JPdJwYZiSvK45G5k3G37RTd6/QyhTlRYXj59RXYajrYngwQA8qMZRkRYcTop\n"
-            + "aG+5M0x44k6NgIyH7Ap+2vRPpDdUlHs+z+6iRvoutkSfKHeZUYBQjgt+tScfn1hM\n"
-            + "BRB+x146ecmSVh/Dh8yu6uCrhitFlKpyJqNptZo5o+sH41zjefpMd/bc8rtHTw3n\n"
-            + "GiFl57ZbXbze2O8UimUVgRI2DtOebt8EAJHM/8vZahzF0chzL4sNVAb8FcNYxAyn\n"
-            + "95VpnWeAtKX7f0bqUvIN4BNV++o6JdMNvBoYEQpKeQIda7QM59hNiS8f/bxkRikF\n"
-            + "OiHB5YGy2zRX5T1G5rVQ0YqrOu959eEwdGZmOQ8GOqq5B/NoHXUtotV6SGE3R+Tl\n"
-            + "grlV4U5/PT0fM3KJATcEHwECACEFAlYmq6wXDIABBK6n7S+CETPlsSjR7QYl3EYy\n"
-            + "iowCBwAACgkQ+tKr4j1sUtDYFgf+Ob63ulm6/xapl1vwWIFr0IV+FxIqNhqsweMH\n"
-            + "DsbFZkkx3lVHgMKvhP9lIAZEoFEpDa5euSb0fQ08OnRz9QoIe1AwUOepOTsHrebB\n"
-            + "JHV3tbSppE9dhDVttq2fOQQM6PR76tfpqKpVEyj+rq4w1QWXpQytRk9I0Hk9VFBY\n"
-            + "+dDsrXWbDe+DR7nJyolvIW8RHoS3INRcYGjxrn/hsrxUE0ExMAjLoZ4al5Izlvty\n"
-            + "lCTv+Ph1e6AIkBV+EU1Q6At26ggozi75VwWOTcqb9jE60aLV0uoJtNf/QuIJED2a\n"
-            + "BCnI1XID5KbjwswtSRtys38XdIdfVqj/mWbX2vwGK7XOJCcuALQiVGVzdHVzZXIg\n"
-            + "U2V2ZW4gPHRlc3Q3QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCViarcQIbAwYLCQgH\n"
-            + "AwIGFQgCCQoLBBYCAwECHgECF4AACgkQ+tKr4j1sUtC+NAf/QrtoGwZTZT+ubgj6\n"
-            + "7649roGcVNhAmqDTfinB0PRZ8zcz9eBWHsRGOdH1lWIIxV6P4reRbl3YM1pe7m9W\n"
-            + "lwPovqONbCIkcHAshyampCsvRN2gKvgXgUpCcFtwfV828/wD+EsMcHeTcRE+8QUi\n"
-            + "jUCRW4riz+/vJlgcCjBfSviiTid27/ABzQcjUWQCmQB+HXwkNO66Lnd2QqGeQOnt\n"
-            + "ed+b8SUBliMHtJ6K/E4P292FWJZLHVWG209mjmFLImsRhonm72Gxc1Q3zkPGAuoK\n"
-            + "ZLW/yfek3WVVj9BNhe2ChxqRCAcYQbROXprfGk2Sf2KGfIKvzSYcLFdPJRQEZ/KB\n"
-            + "xx6UH50DmARWJqtxAQgArHBdvocZX1CN6kAPJZbsYzFzYlVnulkzax1/zCuCdCpX\n"
-            + "YbWtz2ACDBNfiO05jS9IpPW8GrT1ldLeqlgeL9bB+3QqRpf9rt7q8ppsNJ+GKXEL\n"
-            + "Yu4bPb7np/pQ7OOCDB/mTgsVNC+QNRm6bA+Oh4LaGpA7dD/09bf0Loae+wKvkvQx\n"
-            + "F07c9tUkzuUlo6QxsbCDKwStaCJlcqXEyki998iwsLgEDGeNdoLLrMe4Fw8w9pEy\n"
-            + "zuwNEHN65NXbCddJ34hdmB2l3WEXtWS5d6fPA6q8EZLroLcgtbRlMVo62CuWgiRO\n"
-            + "+1gRwkVa/GpaXYX4lDlXp67ZonccgbKv0vVHwxQDoQARAQABAAf5Ae8xa1mPns1E\n"
-            + "B5yCrvzDl79Dw0F1rED46IWIW/ghpVTzmFHV6ngcvcRFM5TZquxHXSuxLv7YVxRq\n"
-            + "UVszXNJaEwyJYYkDRwAS1E2IKN+gknwapm2eWkchySAajUsQt+XEYHFpDPtQRlA3\n"
-            + "Z6PrCOPJDOLmT9Zcf0R6KurGrhvTGrZkKU6ZCFqZWETfZy5cPfq2qxtw3YEUI+eT\n"
-            + "09AgMmPJ9nDPI3cA69tvy/phVFgpglsS76qgd6uFJ5kcDoIB+YepmJoHnzJeowYt\n"
-            + "lvnmmyGqmVS/KCgvILaD0c73Dp2X0BN64hSZHa3nUU67WbKJzo2OXr+yr0hvofcf\n"
-            + "8vhKJe5+2wQAy+rRKSAOPaFiKT8ZenRucx1pTJLoB8JdediOdR4dtXB2Z59Ze7N3\n"
-            + "sedfrJn1ao+jJEpnKeudlDq7oa9THd7ZojN4gBF/lz0duzfertuQ/MrHaTPeK8YI\n"
-            + "dEPg3SgYVOLDBptaKmo0xr2f6aslGLPHgxCgzOcLuuUNGKJSigZvhdMEANh7VKsX\n"
-            + "nb5shZh+KRET84us/uu74q4iIfc8Q10oXuN9+IPlqfAIclo4uMhvo5rtI9ApFtxs\n"
-            + "oZzqqc+gt+OAbn/fHeb61eT36BA+r61Ka+erxkpWU5r1BPVIqq+biTY/HHchqroJ\n"
-            + "aw81qWudO9h5a0yP1alDiBSwhZWIMCKzp6Q7A/472amrSzgs7u8ToQ/2THDxaMf3\n"
-            + "Se0HgMrIT1/+5es2CWiEoZGSZTXlimDYXJULu/DFC7ia7kXOLrMsO85bEi7SHagA\n"
-            + "eO+mAw3xP3OuNkZDt9x4qtal28fNIz22DH5qg2wtsGdCWXz5C6OdcrtQ736kNxa2\n"
-            + "5QemZ/0VWxHPnvXz40RtiQEfBBgBAgAJBQJWJqtxAhsMAAoJEPrSq+I9bFLQzQ8H\n"
-            + "/1FuTj+k1brv64TWXC1ogbu4WIxsmq7yMroyQPwZ1rJ+WuhYRkDHRejD0cvxRpF9\n"
-            + "5E9NkysNGmOX58khdPmqeUHQU+bNJcAqvemILyaKAFxunmbqFaFzdJwCpY54y32P\n"
-            + "RNNYeHiEpLxD6Urf3aG3rlwj+SkWHGb0QRpeZqOmU0v/Ybc9WuV8VSAJ06fYNh1V\n"
-            + "KAmx8GGRyiLLXsbe+q1v5Z7q6WHOJp7hIiuRRKp1iZGOIXK+pnO2YwSgXoJA0JRv\n"
-            + "U0rROxIN4h0NpxiNTDKrRCPpNpetZxsLeRbQa1agVM0iJfir1s34HqlrD+zw/tcJ\n"
-            + "bOdMFF2UVZaCuFynNDx958I=\n"
-            + "=aoJv\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        +B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC
+        =CHer
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        bOdMFF2UVZaCuFynNDx958I=
+        =aoJv
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -799,113 +827,117 @@
    */
   public static TestKey keyRevokedByExpiredKeyAfterExpiration() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBELuRwABCAC56yhFKybBtuKT4nyb7RdLE98pZR54aGjcDcKH3VKVyBF8Z4Kx\n"
-            + "ptd7Sre0mLPCQiNWVOmCT+JG7GKVE6YeFmyXDUnhX9w4+HAeDEh23S4u9JvwWaF+\n"
-            + "wlJ6jLq/oe5gdT1F6Y2yqNpQ6CztOw52Ko9KSYz7/1zBMPcCkl/4k15ee4iebVdq\n"
-            + "c7qT5Qt49Poiozh0DI5prPQ624uckHkz2mXshjWQVuHWwrkIkCJZ2I/KQN2kBjKw\n"
-            + "/ALxumaWmiB9lQ0nIwLuGzHCh0Xg5RxuCrK8fJp47Aza3ikVuYlNzSxhJVav3OtK\n"
-            + "gftBihQXUlY3Uy/4QTCeH/BdVs5OALtXL3VhABEBAAGJAS0EIAECABcFAlYmr4kQ\n"
-            + "HQN0ZXN0OCBub3QgdXNlZAAKCRA87HgbF94azQJ5B/0TeQk7TSChNp+NqCKPTuw0\n"
-            + "wpflDyc+5ru/Gcs4r358cWzgiLUb3M0Q1+M8CF13BFQdrxT05vjheI9o5PCn3b//\n"
-            + "AHV8m+QFSnRi2J3QslbvuOqOnipz7vc7lyZ7q1sWNC33YN+ZcGZiMuu5HJi9iadf\n"
-            + "ZL7AdInpUb4Zb+XKphbMokDcN3yw7rqSMMcx+rKytUAqUnt9qvaSLrIH/zeazxlp\n"
-            + "YG4jaN53WPfLCcGG+Rw56mW+eCQD2rmzaNHCw8Qr+19sokXLB7OML+rd1wNwZT4q\n"
-            + "stWnL+nOj8ZkbFV0w3zClDYaARr7H+vTckwVStyDVRbnpRitSAtJwbRDzZBaS4Vx\n"
-            + "iQE3BB8BAgAhBQJC7lUQFwyAAR2e63ndOLBJk52crzzseBsX3hrNAgcAAAoJEAAa\n"
-            + "34t4v31+AS4H/0x3Y9E3q9DR5FCuYTXG4BHyrALo2WKoP0CfUWL98Fw9Txl0hF+9\n"
-            + "5wriNlnmd2zvM0quHs78x4/xehQO88cw0lqPx3RARq/ju5/VbOjoNlcHvfGYZiEd\n"
-            + "yWOwHu7O8sZrenFDjeDglD6NArrjncOcC51XIPSSTLvVQpSauQ1FS4tan5Q4aWMb\n"
-            + "s4DzE+Vqu2xMkO/X9toYAZKzyWP29OckpouMbt3GUnS6/o0A8Z7jVX+XOIk3XolP\n"
-            + "Li9tzTQB12Xl23mgFvearDoguR2Bu2SbmTJtdiXz8L3S54kGvxVqak5uOP2dagzU\n"
-            + "vBiqR4SVoAdGoXt6TI6mpA+qdYmPMG8v21S0IlRlc3R1c2VyIEVpZ2h0IDx0ZXN0\n"
-            + "OEBleGFtcGxlLmNvbT6JATgEEwECACIFAkLuRwACGwMGCwkIBwMCBhUIAgkKCwQW\n"
-            + "AgMBAh4BAheAAAoJEAAa34t4v31+8/sIAIuqd+dU8k9c5VQ12k7IfZGGYQHF2Mk/\n"
-            + "8FNuP7hFP/VOXBK3QIxIfGEOHbDX6uIxudYMaDmn2UJbdIqJd8NuQByh1gqXdX/x\n"
-            + "nteUa+4e7U6uTjkp/Ij5UzRed8suINA3NzVOy6qwCu3DTOXIZcjiOZtOA5GTqG6Z\n"
-            + "naDP0hwDssJp+LXIYTJgsvneJQFGSdQhhJSv19oV0JPSbb6Zc7gEIHtPcaJHjuZQ\n"
-            + "Ev+TRcRrI9HPTF0MvgOYgIDo2sbcSFV+8moKsHMC+j1Hmuuqgm/1yKGIZrt0V75s\n"
-            + "D9HYu0tiS3+Wlsry3y1hg/2XBQbwgh6sT/jWkpWar7+uzNxO5GdFYrC5AQ0EQu5H\n"
-            + "AAEIALPFTedbfyK+9B35Uo9cPsmFa3mT3qp/bAQtnOjiTTTiIO3tu0ALnaBjf6On\n"
-            + "fAV1HmGz6hRMRK4LGyHkNTaGDNNPoXO7+t9DWycSHmsCL5d5zp7VevQE8MPR8zHK\n"
-            + "Il2YQlCzdy5TWSUhunKd4guDNZ9GiOS6NQ9feYZ9DQ1kzC8nnu7jLkR2zNT02sYU\n"
-            + "kuOCZUktQhVNszUlavdIFjvToZo3RPcdb/E3kTTy2R9xi89AXjWZf3lSAZe3igkL\n"
-            + "jhwsd+u3RRx0ptOJym7zYl5ZdUZk4QrS7FPI6zEBpjawbS4/r6uEW89P3QAkanDI\n"
-            + "ridIAZP8awLZU3uSPtMwPIJpao0AEQEAAYkBHwQYAQIACQUCQu5HAAIbDAAKCRAA\n"
-            + "Gt+LeL99fqpHB/wOXhdMNtgeVW38bLk8YhcEB23FW6fDjFjBJb9m/yqRTh5CIeG2\n"
-            + "bm29ofT4PTamPb8Gt+YuDLnQQ3K2jURakxNDcYwiurvR/oHVdxsBRU7Px7UPeZk3\n"
-            + "BG5VnIJRT198dF7MWFJ+x5wHbNXwM8DDvUwTjXLH/TlGl1XIheSTHCYd9Pra4ejE\n"
-            + "ockkrDaZlPCQdTwY+P7K2ieb5tsqNpJkQeBrglF2bemY/CtQHnM9qwa6ZJqkyYNR\n"
-            + "F1nkSYn36BPuNpytYw1CaQV9GbePugPHtshECLwA160QzqISQUcJlKXttUqUGnoO\n"
-            + "0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F\n"
-            + "=ihWb\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBELuRwABCAC56yhFKybBtuKT4nyb7RdLE98pZR54aGjcDcKH3VKVyBF8Z4Kx\n"
-            + "ptd7Sre0mLPCQiNWVOmCT+JG7GKVE6YeFmyXDUnhX9w4+HAeDEh23S4u9JvwWaF+\n"
-            + "wlJ6jLq/oe5gdT1F6Y2yqNpQ6CztOw52Ko9KSYz7/1zBMPcCkl/4k15ee4iebVdq\n"
-            + "c7qT5Qt49Poiozh0DI5prPQ624uckHkz2mXshjWQVuHWwrkIkCJZ2I/KQN2kBjKw\n"
-            + "/ALxumaWmiB9lQ0nIwLuGzHCh0Xg5RxuCrK8fJp47Aza3ikVuYlNzSxhJVav3OtK\n"
-            + "gftBihQXUlY3Uy/4QTCeH/BdVs5OALtXL3VhABEBAAEAB/wLr88oGuxsoqIHRQZL\n"
-            + "eGm9jc4aQGmcDMcjpwdGilhrwyfrO6f84hWbQdD+rJcnI8hsH7oOd5ZMGkWfpJyt\n"
-            + "eUAh9iNB5ChYGfDVSLUg6KojqDtprj6vNMihvLkr/OI6xL/hZksikwfnLFMPpgXU\n"
-            + "knwPocQ3nn+egsUSL7CR8/SLiIm4MC0brer6jhDxB5LKweExNlfTe4c0MDeYTsWt\n"
-            + "0WGzNPlvRZQXRotJzqemt3wdNZXUnCKR0n7pSQ8EhZr2O6NXr+mUgp6PIOE/3un2\n"
-            + "YGiBEf5uy3qEFe7FjEGIHz+Z3ySRdUDfHOk82TKAzynoJIxRUvLIYVNw4eFB3l5U\n"
-            + "s1w5BADUzfciG7RVLa8UFKJfqQ/5M06QmdS1v1/hMQXg38+3vKe8RgfSSnMJ08Sc\n"
-            + "eAEsmugwpNXAxgRKHcmWzN3NMBHhE3KiyiogWaMGqmSo6swFpu0+dwMvZSxMlfD+\n"
-            + "ka/BWt8YsUdrqW06ow39aTgCV+icbNRV81C7NKe7u0X1JDx2CQQA36gbdo62h/Wd\n"
-            + "gJI8kdz/se3xrt8x6RoWvOnWPNmsZR5XkDqAMTL1dWiEEA/dQTphMcgAe9z3WaP+\n"
-            + "F1TPAfounbiurGCcS3kxJ5tY7ojyU7nYz4DA/V2OU0C/LUoLXhttG5HM+m/i3qn4\n"
-            + "K9bBoWIQY1ijliS7cTSwNqd6IHaQGpkEAMnp5GwSGhY+kUuLw06hmH4xnsuf6agz\n"
-            + "AfhbPylB2nf/ZaX6dt6/mFEAkvQNahcoWEskfS3LGCD8jHm8PvF8K0mciXPDweq2\n"
-            + "gW3/irE0RXNwn3Oa222VSvcgUlocBm9InkfvpFXh20OYFe3dFH7uYkwUqIHJeXjw\n"
-            + "TjpXUX/vC5QJQOyJATcEHwECACEFAkLuVRAXDIABHZ7red04sEmTnZyvPOx4Gxfe\n"
-            + "Gs0CBwAACgkQABrfi3i/fX4BLgf/THdj0Ter0NHkUK5hNcbgEfKsAujZYqg/QJ9R\n"
-            + "Yv3wXD1PGXSEX73nCuI2WeZ3bO8zSq4ezvzHj/F6FA7zxzDSWo/HdEBGr+O7n9Vs\n"
-            + "6Og2Vwe98ZhmIR3JY7Ae7s7yxmt6cUON4OCUPo0CuuOdw5wLnVcg9JJMu9VClJq5\n"
-            + "DUVLi1qflDhpYxuzgPMT5Wq7bEyQ79f22hgBkrPJY/b05ySmi4xu3cZSdLr+jQDx\n"
-            + "nuNVf5c4iTdeiU8uL23NNAHXZeXbeaAW95qsOiC5HYG7ZJuZMm12JfPwvdLniQa/\n"
-            + "FWpqTm44/Z1qDNS8GKpHhJWgB0ahe3pMjqakD6p1iY8wby/bVLQiVGVzdHVzZXIg\n"
-            + "RWlnaHQgPHRlc3Q4QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCQu5HAAIbAwYLCQgH\n"
-            + "AwIGFQgCCQoLBBYCAwECHgECF4AACgkQABrfi3i/fX7z+wgAi6p351TyT1zlVDXa\n"
-            + "Tsh9kYZhAcXYyT/wU24/uEU/9U5cErdAjEh8YQ4dsNfq4jG51gxoOafZQlt0iol3\n"
-            + "w25AHKHWCpd1f/Ge15Rr7h7tTq5OOSn8iPlTNF53yy4g0Dc3NU7LqrAK7cNM5chl\n"
-            + "yOI5m04DkZOobpmdoM/SHAOywmn4tchhMmCy+d4lAUZJ1CGElK/X2hXQk9Jtvplz\n"
-            + "uAQge09xokeO5lAS/5NFxGsj0c9MXQy+A5iAgOjaxtxIVX7yagqwcwL6PUea66qC\n"
-            + "b/XIoYhmu3RXvmwP0di7S2JLf5aWyvLfLWGD/ZcFBvCCHqxP+NaSlZqvv67M3E7k\n"
-            + "Z0VisJ0DmARC7kcAAQgAs8VN51t/Ir70HflSj1w+yYVreZPeqn9sBC2c6OJNNOIg\n"
-            + "7e27QAudoGN/o6d8BXUeYbPqFExErgsbIeQ1NoYM00+hc7v630NbJxIeawIvl3nO\n"
-            + "ntV69ATww9HzMcoiXZhCULN3LlNZJSG6cp3iC4M1n0aI5Lo1D195hn0NDWTMLyee\n"
-            + "7uMuRHbM1PTaxhSS44JlSS1CFU2zNSVq90gWO9OhmjdE9x1v8TeRNPLZH3GLz0Be\n"
-            + "NZl/eVIBl7eKCQuOHCx367dFHHSm04nKbvNiXll1RmThCtLsU8jrMQGmNrBtLj+v\n"
-            + "q4Rbz0/dACRqcMiuJ0gBk/xrAtlTe5I+0zA8gmlqjQARAQABAAf+JNVkZOcGYaQm\n"
-            + "eI3BMMaBxuCjaMG3ec+p3iFKaR0VHKTIgneXSkQXA+nfGTUT4DpjAznN2GLYH6D+\n"
-            + "6i7MCGPm9NT4C7KUcHJoltTLjrlf7vVyNHEhRCZO/pBh9+2mpO6xh799x+wj88u5\n"
-            + "XAqlah50OjJFkjfk70VsrPWqWvgwLejkaQpGbE+pdL+vjy+ol5FHzidzmJvsXDR1\n"
-            + "I1as0vBu5g2XPpexyVanmHJglZdZX07OPYQBhxQKuPXT/2/IRnXsXEpitk4IyJT0\n"
-            + "U5D/iedEUldhBByep1lBcJnAap0CP7iuu2CYhRp6V2wVvdweNPng5Eo7f7LNyjnX\n"
-            + "UMAeaeCjAQQA1A0iKtg3Grxc9+lpFl1znc2/kO3p6ixM13uUvci+yGFNJJninnxo\n"
-            + "99KXEzqqVD0zerjiyyegQmzpITE/+hFIOJZInxEH08WQwZstV/KYeRSJkXf0Um48\n"
-            + "E+Zrh8fpJVW1w3ZCw9Ee2yE6fEhAA4w66+50pM+vBXanWOrG1HDrkxEEANkHc2Rz\n"
-            + "YJsO4v63xo/7/njLSQ31miOglb99ACKBA0Yl/jvj2KqLcomKILqvK3DKP+BHNq86\n"
-            + "LUBUglyKjKuj0wkSWT0tCnfgLzysUpowcoyFhJ36KzAz8hjqIn3TQpMF21HvkZdG\n"
-            + "Mtkcyhu5UDvbfOuWOBaKIeNQWCWv1rNzMme9A/9zU1+esEhKwGWEqa3/B/Te/xQh\n"
-            + "alk180n74sTZid6lXD8o8cEei0CUq7zBSV0P8v6kk8PP9/XyLRl3Rqa95fESUWrL\n"
-            + "xD6TBY1JlHBZS+N6rN/7Ilf5EXSELmnbDFsVxkNGp4elKxajvZxC6uEWYBu62AYy\n"
-            + "wS0dj8mZR3faCEps90YXiQEfBBgBAgAJBQJC7kcAAhsMAAoJEAAa34t4v31+qkcH\n"
-            + "/A5eF0w22B5VbfxsuTxiFwQHbcVbp8OMWMElv2b/KpFOHkIh4bZubb2h9Pg9NqY9\n"
-            + "vwa35i4MudBDcraNRFqTE0NxjCK6u9H+gdV3GwFFTs/HtQ95mTcEblWcglFPX3x0\n"
-            + "XsxYUn7HnAds1fAzwMO9TBONcsf9OUaXVciF5JMcJh30+trh6MShySSsNpmU8JB1\n"
-            + "PBj4/sraJ5vm2yo2kmRB4GuCUXZt6Zj8K1Aecz2rBrpkmqTJg1EXWeRJiffoE+42\n"
-            + "nK1jDUJpBX0Zt4+6A8e2yEQIvADXrRDOohJBRwmUpe21SpQaeg7R3Q/LNlPfrvqZ\n"
-            + "DCYWh5sxH28AIB4eO8PEPgU=\n"
-            + "=cSfw\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F
+        =ihWb
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        DCYWh5sxH28AIB4eO8PEPgU=
+        =cSfw
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -921,113 +953,117 @@
    */
   public static TestKey keyRevokedByExpiredKeyBeforeExpiration() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBELuRwABCADnf2z5dqp3BMFlpd6iUs5dhROrslfzswak1LmbGirK2IPIl4NX\n"
-            + "arAi76xXK9BcF/Cqcj/X/WqFKBd/qMGxwdvwbSN6PVBP6T1jvuVgrPTjd4x5xPUD\n"
-            + "xZ5VPy9hgQXs+1mugTkHYVTU8GI1eGpZ8Oj3PJIgVyqGxGkjWmcz5APbVIRan6L1\n"
-            + "482bZTidH9Nd9YnYlXNgiJcaOPAVBwO/j/myocQCIohvIo4IT8vc/ODhRgfwA0gD\n"
-            + "GVK+tXwT4f4x3qjG/YRpOOZZjBS09B/gJ9QfEnR6WNxg/Tm3T0uipoISOhR+cP/V\n"
-            + "e5o/73SM+w+WlILk/xpbbOfyCxD4Q3lb8EZFABEBAAGJAS0EIAECABcFAkLuYyAQ\n"
-            + "HQN0ZXN0OSBub3QgdXNlZAAKCRA87HgbF94azV2BB/9Rc1j3XOxKbDyUFAORAGnE\n"
-            + "ezQtpOmQhaSUhFC35GFOdTg4eX53FTFSXLJQleTVzvE+eVkQI5tvUZ+SqHoyjnhU\n"
-            + "DpWlmfRUQy4GTUjUTkpFOK07TVTjhUQwaAxN13UZgByopVKc7hLf+uh1xkRJIqAJ\n"
-            + "Tx6LIFZiSIGwStDO6TJlhl1e8h45J3rAV4N+DsGpMy9S4uYOU7erJDupdXK739/l\n"
-            + "VBsP2SeT85iuAv+4A9Jq3+iq+cjK9q3QZCw1O6iI2v3seAWCI6HH3tVw4THr+M6T\n"
-            + "EdTGmyESjdAl+f7/uK0QNfqIMpvUf+AvMakrLi7WOeDs8mpUIjonpeQVLfz6I0Zo\n"
-            + "iQE3BB8BAgAhBQJC7lUQFwyAAR2e63ndOLBJk52crzzseBsX3hrNAgcAAAoJEGBA\n"
-            + "hmHEO/LhHjUH/R/7+iNBLAfKYbpprkWy/8eXVEJhxfh6DI/ppsKLIA+687gX74R9\n"
-            + "6CM5k6fZDjeND26ZEA0rDZmYrbnGUfsu55aeM0/+jiSOZJ2uTlrLXiHMurbNY0pT\n"
-            + "xv215muhumPBzuL1jsAK2Kc/4oE7Z46jaStsPCvDOcx9PW76wR8/uCPvHVz5H/A7\n"
-            + "3erXAloC43jupXwZB32VZq8L0kZNVfuEsjHUcu3GUoZdGfTb4/Qq5a1FK+CGhwWC\n"
-            + "OwpUWZEIUImwUv4FNE4iNFYEHaHLU9fotmIxIkH8TC4NcO+GvkEyMyJ6NVkBBDP2\n"
-            + "EarncWAJxDBlx1CO4ET+/ULvzDnAcYuTc6G0IVRlc3R1c2VyIE5pbmUgPHRlc3Q5\n"
-            + "QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCQu5HAAIbAwYLCQgHAwIGFQgCCQoLBBYC\n"
-            + "AwECHgECF4AACgkQYECGYcQ78uG78ggA1TjeOZtaXjXNG8Bx2sl4W+ypylWWB6yc\n"
-            + "IeR0suLhVlisZ33yOtV4MsvZw0TJNyYmFXiskPTyOcP8RJjS+a41IHc33i13MUnN\n"
-            + "RI5cqhqsWRhf9chlm7XqXtqv57IjojG9vgSUeZdXSTMdHIDDHAjJ/ryBXflzprSw\n"
-            + "2Sab8OXjLkyo9z6ZytFyfXSc8TNiWU6Duollh/bWIsgPETIe2wGn8LcFiVMfPpsI\n"
-            + "RhkphOdTJb+W/zQwLHUcS22A4xsJtBxIXTH/QSG3lAaw8IRbl25EIpaEAF+gExCr\n"
-            + "QM0haAVMmGgYYWpMHXrDhB7ff3kAiqD2qmhSySA6NLmTO+6qGPYJg7kBDQRC7kcA\n"
-            + "AQgA2wqE3DypQhTcYl26dXc9DZzABRQa6KFRqQbhmUBz95cQpAamQjrwOyl2fg84\n"
-            + "b9o9t+DuZcdLzLF/gPVSznOcNUV9mJNdLAxBPPOMUrP/+Snb83FkNpCscrXhIqSf\n"
-            + "BU5D+FOb3bEI2WTJ7lLe8oCrWPE3JIDVCrpAWgZk9puAk1Z7ZFaHsS6ezsZP0YIM\n"
-            + "qTWdoX0zHMPMnr9GG08c0mniXtvfcgtOCeIRU4WZws28sGYCoLeQXsHVDal+gcLp\n"
-            + "1enPh6dfEWBJuhhBBajzm53fzV2a7khEdffggVVylHPLpvms2nIqoearDQtVNpSK\n"
-            + "uhNiykJSMIUn/Y6g5LMySmL+MwARAQABiQEfBBgBAgAJBQJC7kcAAhsMAAoJEGBA\n"
-            + "hmHEO/LhdwcH/0wAxT1NGaR2boMjpTouVUcnEcEzHc0dSwuu+06mLRggSdAfBC8C\n"
-            + "9fdlAYHQ5tp1sRuPwLfQZjo8wLxJ+wLASnIPLaGrtpEHkIKvDwHqwkOXvXeGD/Bh\n"
-            + "40NbJUa7Ec3Jpo+FPFlM8hDsUyHf8IhUAdRd4d+znOVEaZ6S7c1RrtoVTUqzi59n\n"
-            + "nC6ZewL/Jp+znKZlMTM3X1onAGhd+/XdrS52LM8pE3xRjbTLTYWcjnjyLbm0yoO8\n"
-            + "G3yCfIibAaII4a/jGON2X9ZUwaFNIqJ4iIc8Nme86rD/flXsu6Zv+NXVQWylrIG/\n"
-            + "REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=\n"
-            + "=FnZg\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBELuRwABCADnf2z5dqp3BMFlpd6iUs5dhROrslfzswak1LmbGirK2IPIl4NX\n"
-            + "arAi76xXK9BcF/Cqcj/X/WqFKBd/qMGxwdvwbSN6PVBP6T1jvuVgrPTjd4x5xPUD\n"
-            + "xZ5VPy9hgQXs+1mugTkHYVTU8GI1eGpZ8Oj3PJIgVyqGxGkjWmcz5APbVIRan6L1\n"
-            + "482bZTidH9Nd9YnYlXNgiJcaOPAVBwO/j/myocQCIohvIo4IT8vc/ODhRgfwA0gD\n"
-            + "GVK+tXwT4f4x3qjG/YRpOOZZjBS09B/gJ9QfEnR6WNxg/Tm3T0uipoISOhR+cP/V\n"
-            + "e5o/73SM+w+WlILk/xpbbOfyCxD4Q3lb8EZFABEBAAEAB/9GTcWLkUU9tf0B4LjX\n"
-            + "NSyk7ChIKXZadVEcN9pSR0Udq1mCTrk9kBID2iPNqWmyvjaBnQbUkoqJ+93/EAIa\n"
-            + "+NPRlWOD2SEN07ioFS5WCNCqUAEibfU2+woVu4WpJ+TjzoWy4F2wZxe7P3Gj6Xjq\n"
-            + "7aXih8uc9Lveh8GiUe8rrCCbt+BH1RzuV/khZw+2ZDPMCx7yfcfKobc3NWx75WLh\n"
-            + "pki512fawSC6eJHRI50ilPrqAmmhcccfwPji9P+oPj2S6wlhe5kp3R5yU85fWy3b\n"
-            + "C8AtLTfZIn4v6NAtBaurGEjRjzeNEGMJHxnRPWvFc4iD+xvPg6SNPJM/bbTE+yZ3\n"
-            + "16W1BADxjAQLMuGpemaVmOpZ3K02hcNjwniEK2QPp11BnfoQCIwegON+sUD/6AuZ\n"
-            + "S1vOVvS3//eGbPaMM45FK/SQAVHpC9IOL4Tql0C8B6csRhFL824yPfc3WDb4kayQ\n"
-            + "T5oLjlJ0W2r7tWcBcREEzZT6gNi4KI7C4oFF6tU9lsQJuQyAbwQA9Vl6VW/7oG0W\n"
-            + "CC+lcHJc+4rxUB3yak7d4mEccTNb+crOBRH/7dKZOe7A6Fz+ra++MmucDUzsAx0K\n"
-            + "MGT9Xoi5+CBBaNr+Y2lB9fF20N7eRNzQ3Xrz2OPl4cmU4gfECTZ1vZaKlmB+Vt8C\n"
-            + "E/nn49QGRI+BNBOdW+2aEpPoENczFosEAJXi5Cn2l0jOswDD7FU2PER1wfVY629i\n"
-            + "bICunudOSo64GKQslKkQWktc57DgdOQnH15qW1nVO7Z4H0GBxjSTRCu7Z7q08/qM\n"
-            + "ueWIvJ85HcFhOCl+vITOn0fZV0p8/IwsWz8G9h5bb2QgMAwDSdhnLuK/cXaGM09w\n"
-            + "n6k8O2rCvDtXRjqJATcEHwECACEFAkLuVRAXDIABHZ7red04sEmTnZyvPOx4Gxfe\n"
-            + "Gs0CBwAACgkQYECGYcQ78uEeNQf9H/v6I0EsB8phummuRbL/x5dUQmHF+HoMj+mm\n"
-            + "wosgD7rzuBfvhH3oIzmTp9kON40PbpkQDSsNmZitucZR+y7nlp4zT/6OJI5kna5O\n"
-            + "WsteIcy6ts1jSlPG/bXma6G6Y8HO4vWOwArYpz/igTtnjqNpK2w8K8M5zH09bvrB\n"
-            + "Hz+4I+8dXPkf8Dvd6tcCWgLjeO6lfBkHfZVmrwvSRk1V+4SyMdRy7cZShl0Z9Nvj\n"
-            + "9CrlrUUr4IaHBYI7ClRZkQhQibBS/gU0TiI0VgQdoctT1+i2YjEiQfxMLg1w74a+\n"
-            + "QTIzIno1WQEEM/YRqudxYAnEMGXHUI7gRP79Qu/MOcBxi5NzobQhVGVzdHVzZXIg\n"
-            + "TmluZSA8dGVzdDlAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJC7kcAAhsDBgsJCAcD\n"
-            + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBgQIZhxDvy4bvyCADVON45m1peNc0bwHHa\n"
-            + "yXhb7KnKVZYHrJwh5HSy4uFWWKxnffI61Xgyy9nDRMk3JiYVeKyQ9PI5w/xEmNL5\n"
-            + "rjUgdzfeLXcxSc1EjlyqGqxZGF/1yGWbtepe2q/nsiOiMb2+BJR5l1dJMx0cgMMc\n"
-            + "CMn+vIFd+XOmtLDZJpvw5eMuTKj3PpnK0XJ9dJzxM2JZToO6iWWH9tYiyA8RMh7b\n"
-            + "AafwtwWJUx8+mwhGGSmE51Mlv5b/NDAsdRxLbYDjGwm0HEhdMf9BIbeUBrDwhFuX\n"
-            + "bkQiloQAX6ATEKtAzSFoBUyYaBhhakwdesOEHt9/eQCKoPaqaFLJIDo0uZM77qoY\n"
-            + "9gmDnQOYBELuRwABCADbCoTcPKlCFNxiXbp1dz0NnMAFFBrooVGpBuGZQHP3lxCk\n"
-            + "BqZCOvA7KXZ+Dzhv2j234O5lx0vMsX+A9VLOc5w1RX2Yk10sDEE884xSs//5Kdvz\n"
-            + "cWQ2kKxyteEipJ8FTkP4U5vdsQjZZMnuUt7ygKtY8TckgNUKukBaBmT2m4CTVntk\n"
-            + "VoexLp7Oxk/RggypNZ2hfTMcw8yev0YbTxzSaeJe299yC04J4hFThZnCzbywZgKg\n"
-            + "t5BewdUNqX6BwunV6c+Hp18RYEm6GEEFqPObnd/NXZruSER19+CBVXKUc8um+aza\n"
-            + "ciqh5qsNC1U2lIq6E2LKQlIwhSf9jqDkszJKYv4zABEBAAEAB/0c76POOw6aazUT\n"
-            + "TZHUnhQ+WHHJefbKuoeWI7w+dD7y+02NzaRoZW7XnJ+fAZW8Dlb5k/O1FayUIEgE\n"
-            + "GjnT336dpE4g5NQkfdifG7Fy5NKGRkWx6viJI3g/OHsYX3+ebNDFMmO0gq7067/9\n"
-            + "WuHsTpvUMRwkF1zi1j4AETjZ7IBXdjuSCSu8OhEwr3d+WXibEmY5ec/d24l/APJx\n"
-            + "c3RMHw9PiDQeAKrByS6N10/yFgRpnouVx3wC7zFmhVewNV476Nyg34OvRoc+lCtk\n"
-            + "ixKdua6KuUJzGRWxgw+q2JD4goXxe0v2qU2KSU63gOYi0kg9tpwpn98lDNQykgmJ\n"
-            + "aQYdNIZJBADdlbkg9qbH1DREs7UF4jXN/SoYRbTh9639GfA4zkbfPmh/RmVIIEKd\n"
-            + "QN7qWK/Xy1bUS9vDzRfFgmoYGtqMmygOOFsVtfm8Y18lSXopN/3vhtai+dn+04Ef\n"
-            + "dl1irmGvm3p7y9Jh3s6uYTEJok0MywA7qBHvgSTVtc1PcZc6j6Bz1QQA/Q+nqyZY\n"
-            + "fLimt4KVYO1y6kSHgEqzggLTxyfGMW5RplTA0V1zCwjM6S+QWNqRxVNdB9Kkzn+S\n"
-            + "YDKHLYs8lXO2zvf8Yk9M7glgqvT4rJ51Zn2rc6lg1YUwFBXup5idTsuZwtqkvvKJ\n"
-            + "eS7L3cSBCqJMRjk47Y3V8zkrrN/HcYmyFecD/A+HPf4eSweUS025Bb+eCk4gTHbR\n"
-            + "uwmnKq7npk2XY4m0A/QdYF9dEWlpadsAr+ZwNQB3f21nQgKG0BudfL4FmpeW9RMt\n"
-            + "35aSIaV7RkxYOt5HEvjFRvLbeL1YYaj+D0dvz8SP1AUPvpWIVlQ03OjRlPyrPW50\n"
-            + "LoqyP8PTb6svnHvmQseJAR8EGAECAAkFAkLuRwACGwwACgkQYECGYcQ78uF3Bwf/\n"
-            + "TADFPU0ZpHZugyOlOi5VRycRwTMdzR1LC677TqYtGCBJ0B8ELwL192UBgdDm2nWx\n"
-            + "G4/At9BmOjzAvEn7AsBKcg8toau2kQeQgq8PAerCQ5e9d4YP8GHjQ1slRrsRzcmm\n"
-            + "j4U8WUzyEOxTId/wiFQB1F3h37Oc5URpnpLtzVGu2hVNSrOLn2ecLpl7Av8mn7Oc\n"
-            + "pmUxMzdfWicAaF379d2tLnYszykTfFGNtMtNhZyOePItubTKg7wbfIJ8iJsBogjh\n"
-            + "r+MY43Zf1lTBoU0ioniIhzw2Z7zqsP9+Vey7pm/41dVBbKWsgb9ERbrzCydaPBO2\n"
-            + "s8bxuqjpwKw7OoZhW3zV5Q==\n"
-            + "=JxsF\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=
+        =FnZg
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        s8bxuqjpwKw7OoZhW3zV5Q==
+        =JxsF
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -1043,67 +1079,71 @@
    */
   public static TestKey validKeyWithoutExpirationWithSubkeyWithExpiration() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "\n"
-            + "mI0EW+98GgEEALLn87xX++daic3AzKwM7nY50Mx2eTaEZPlnaDAVvFlhbZuPG+n5\n"
-            + "g93vYX3wEfnFxI7IBEe7VMT1AyszLZgpFmbzW8eGQxCGpRd1hYrUUlC0IkGAwG9v\n"
-            + "LQB85GDDZUUH4p+A4oHX0yUm8iCpbO9+D82xzNDe/D8Xbw1foWMWGonLABEBAAG0\n"
-            + "JFRlc3R1c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEE\n"
-            + "VzQsN5gqhDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwEC\n"
-            + "HgECF4AACgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5v\n"
-            + "jpAjwusIHRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7\n"
-            + "fDgfqnNa5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/\n"
-            + "8v7ABV24jQRb73xXAQQAssv5gwxWx5J0q4gGcqMIaJKzBaHAjiK3ryH6qnFQpsf1\n"
-            + "ODtU+a4NxFJsXGOd6jHEhBEHPgWAaaKZ7PEJVnwA/XOhPG+q9YimAbbZS0qmC/LH\n"
-            + "DpFtFbsJsMKZbIC69j9OcbmalIowspFQBVeAankGFReZVhh99Z/o81Y+Twm9eisA\n"
-            + "EQEAAYkBcQQYAQgAJhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlY\n"
-            + "WHSAAL8JEGqvLSa0gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUC\n"
-            + "W+98VwAKCRBDBckuYm60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oup\n"
-            + "TimR1cVgN1CEAWh82UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQK\n"
-            + "mXGWM7Q33hU1E7IfgU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuo\n"
-            + "qRwqjHt2BACuXwA8RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Ws\n"
-            + "qdx2vbbJxy1bfTYxb0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOy\n"
-            + "ziatLaUBSQfyG656lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
-            + "=BG9Z\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "\n"
-            + "lQIGBFvvfBoBBACy5/O8V/vnWonNwMysDO52OdDMdnk2hGT5Z2gwFbxZYW2bjxvp\n"
-            + "+YPd72F98BH5xcSOyARHu1TE9QMrMy2YKRZm81vHhkMQhqUXdYWK1FJQtCJBgMBv\n"
-            + "by0AfORgw2VFB+KfgOKB19MlJvIgqWzvfg/NsczQ3vw/F28NX6FjFhqJywARAQAB\n"
-            + "/gcDAmvJS+mrsxlf7D58lnhHw8gBjP5JkY9anTgVhIdieJEIlitV4PyRQYPAGZZd\n"
-            + "asUC7bC7WnLCygiU59kXS5z63Ue/RM0tVwWy0FsigpseC90Mtwb4wjL2jTeebszD\n"
-            + "UcM33d6Tg9s4eNnsHzpmlC/CReW6MYJj0/06AsvgUgOxgWXf0YapOLRIr60reTUb\n"
-            + "ovVZtH76rsZXyQvR9qJv11F+BmIDDzg4EsipXDGVuEZ0SXJyq6OLAUPkV2ZdELaT\n"
-            + "P4RWp0Zsn22H8jm4MsZ7la2Ux3jD2AMdy2B9dpwhuxOegDSXUMRfXYwxQ1wioqpA\n"
-            + "pOZ1RjjFsID34XNtxGp3wMYcFleOl3CSpXyW/P1PYTVBQta/Y7xniEetzUk9NHVc\n"
-            + "2jMD8a8767+Tk0SChIPmWOhQYrHS1Ce309SjTRSRVexjKF0Mp5WXHxqz582IlPT9\n"
-            + "jdxvLjVIW4xbtZmnQ2JnPHInWbxoa3exaoPg5osvg8h7QlGxRY6H2/20JFRlc3R1\n"
-            + "c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEEVzQsN5gq\n"
-            + "hDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA\n"
-            + "CgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5vjpAjwusI\n"
-            + "HRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7fDgfqnNa\n"
-            + "5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/8v7ABV2d\n"
-            + "AgYEW+98VwEEALLL+YMMVseSdKuIBnKjCGiSswWhwI4it68h+qpxUKbH9Tg7VPmu\n"
-            + "DcRSbFxjneoxxIQRBz4FgGmimezxCVZ8AP1zoTxvqvWIpgG22UtKpgvyxw6RbRW7\n"
-            + "CbDCmWyAuvY/TnG5mpSKMLKRUAVXgGp5BhUXmVYYffWf6PNWPk8JvXorABEBAAH+\n"
-            + "BwMClgBcY/ItnafslgEWZOSqsxCSJatN72c9zzWZE+zmcx9NbDRuCVxXhTbHJZZs\n"
-            + "Hz44vsKqOKtyhrfr9Oke0mYyzH2CX6tv6ghJyC6znRCGoc/P84uJ1v3ibO/7/p85\n"
-            + "PDHzEEpHmdbef+UymnjZBYKGi45SINy3bLwOa/Vl80Q4wsppPe9oynerq6ig94HR\n"
-            + "e3mNDw/JggtgJA0X2VmmmXG8vHwIB5EziQrH7QGtLyjqdE+w7CLbbvAskL8Uw1qx\n"
-            + "Aowdpb7J8hrUdIDDCr/mlhT17+UM5yOXHKcixyrscqbjlG/nqwPvR10efo7D0rFR\n"
-            + "6tu5OU2y3N2PhGOysDLgupUXBLlpdByF6AYNV9zvU7ipO7QXzrUfYCb/WyAcjl+X\n"
-            + "Yl38sCVTVFGsB2ql9/fzFCxAB3FUNHDlI2sUbkdDPcjgf65SK0GGcckWfntfq9dj\n"
-            + "pQzEVen8X9dT3UhfuvHd98g3n6ju9gh8NucwHM5jITq9ItTY0whb+okBcQQYAQgA\n"
-            + "JhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlYWHSAAL8JEGqvLSa0\n"
-            + "gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUCW+98VwAKCRBDBcku\n"
-            + "Ym60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oupTimR1cVgN1CEAWh8\n"
-            + "2UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQKmXGWM7Q33hU1E7If\n"
-            + "gU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuoqRwqjHt2BACuXwA8\n"
-            + "RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Wsqdx2vbbJxy1bfTYx\n"
-            + "b0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOyziatLaUBSQfyG656\n"
-            + "lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==\n"
-            + "=RbPm\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+        mI0EW+98GgEEALLn87xX++daic3AzKwM7nY50Mx2eTaEZPlnaDAVvFlhbZuPG+n5
+        g93vYX3wEfnFxI7IBEe7VMT1AyszLZgpFmbzW8eGQxCGpRd1hYrUUlC0IkGAwG9v
+        LQB85GDDZUUH4p+A4oHX0yUm8iCpbO9+D82xzNDe/D8Xbw1foWMWGonLABEBAAG0
+        JFRlc3R1c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEE
+        VzQsN5gqhDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwEC
+        HgECF4AACgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5v
+        jpAjwusIHRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7
+        fDgfqnNa5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/
+        8v7ABV24jQRb73xXAQQAssv5gwxWx5J0q4gGcqMIaJKzBaHAjiK3ryH6qnFQpsf1
+        ODtU+a4NxFJsXGOd6jHEhBEHPgWAaaKZ7PEJVnwA/XOhPG+q9YimAbbZS0qmC/LH
+        DpFtFbsJsMKZbIC69j9OcbmalIowspFQBVeAankGFReZVhh99Z/o81Y+Twm9eisA
+        EQEAAYkBcQQYAQgAJhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlY
+        WHSAAL8JEGqvLSa0gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUC
+        W+98VwAKCRBDBckuYm60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oup
+        TimR1cVgN1CEAWh82UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQK
+        mXGWM7Q33hU1E7IfgU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuo
+        qRwqjHt2BACuXwA8RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Ws
+        qdx2vbbJxy1bfTYxb0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOy
+        ziatLaUBSQfyG656lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==
+        =BG9Z
+        -----END PGP PUBLIC KEY BLOCK-----\
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+        lQIGBFvvfBoBBACy5/O8V/vnWonNwMysDO52OdDMdnk2hGT5Z2gwFbxZYW2bjxvp
+        +YPd72F98BH5xcSOyARHu1TE9QMrMy2YKRZm81vHhkMQhqUXdYWK1FJQtCJBgMBv
+        by0AfORgw2VFB+KfgOKB19MlJvIgqWzvfg/NsczQ3vw/F28NX6FjFhqJywARAQAB
+        /gcDAmvJS+mrsxlf7D58lnhHw8gBjP5JkY9anTgVhIdieJEIlitV4PyRQYPAGZZd
+        asUC7bC7WnLCygiU59kXS5z63Ue/RM0tVwWy0FsigpseC90Mtwb4wjL2jTeebszD
+        UcM33d6Tg9s4eNnsHzpmlC/CReW6MYJj0/06AsvgUgOxgWXf0YapOLRIr60reTUb
+        ovVZtH76rsZXyQvR9qJv11F+BmIDDzg4EsipXDGVuEZ0SXJyq6OLAUPkV2ZdELaT
+        P4RWp0Zsn22H8jm4MsZ7la2Ux3jD2AMdy2B9dpwhuxOegDSXUMRfXYwxQ1wioqpA
+        pOZ1RjjFsID34XNtxGp3wMYcFleOl3CSpXyW/P1PYTVBQta/Y7xniEetzUk9NHVc
+        2jMD8a8767+Tk0SChIPmWOhQYrHS1Ce309SjTRSRVexjKF0Mp5WXHxqz582IlPT9
+        jdxvLjVIW4xbtZmnQ2JnPHInWbxoa3exaoPg5osvg8h7QlGxRY6H2/20JFRlc3R1
+        c2VyIDEwIDx0ZXN0dXNlcjEwQGV4YW1wbGUuY29tPojOBBMBCAA4FiEEVzQsN5gq
+        hDsZwGIraq8tJrSBAtsFAlvvfBoCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA
+        CgkQaq8tJrSBAtvfFAP/VqV77KQZp9rjSGStDpxxlatr4Y5nrRBZfV5vjpAjwusI
+        HRjr0OWXLxX7NDLYd+oIjhLFn26Lux1UXOQT+rGRPwnxoJZWrpDQidP7fDgfqnNa
+        5UQGvoBPSIVEK1l0DlYAOUuciwz3HdMkeMuvEVEdyg7nOiVd1bF9V9i/8v7ABV2d
+        AgYEW+98VwEEALLL+YMMVseSdKuIBnKjCGiSswWhwI4it68h+qpxUKbH9Tg7VPmu
+        DcRSbFxjneoxxIQRBz4FgGmimezxCVZ8AP1zoTxvqvWIpgG22UtKpgvyxw6RbRW7
+        CbDCmWyAuvY/TnG5mpSKMLKRUAVXgGp5BhUXmVYYffWf6PNWPk8JvXorABEBAAH+
+        BwMClgBcY/ItnafslgEWZOSqsxCSJatN72c9zzWZE+zmcx9NbDRuCVxXhTbHJZZs
+        Hz44vsKqOKtyhrfr9Oke0mYyzH2CX6tv6ghJyC6znRCGoc/P84uJ1v3ibO/7/p85
+        PDHzEEpHmdbef+UymnjZBYKGi45SINy3bLwOa/Vl80Q4wsppPe9oynerq6ig94HR
+        e3mNDw/JggtgJA0X2VmmmXG8vHwIB5EziQrH7QGtLyjqdE+w7CLbbvAskL8Uw1qx
+        Aowdpb7J8hrUdIDDCr/mlhT17+UM5yOXHKcixyrscqbjlG/nqwPvR10efo7D0rFR
+        6tu5OU2y3N2PhGOysDLgupUXBLlpdByF6AYNV9zvU7ipO7QXzrUfYCb/WyAcjl+X
+        Yl38sCVTVFGsB2ql9/fzFCxAB3FUNHDlI2sUbkdDPcjgf65SK0GGcckWfntfq9dj
+        pQzEVen8X9dT3UhfuvHd98g3n6ju9gh8NucwHM5jITq9ItTY0whb+okBcQQYAQgA
+        JhYhBFc0LDeYKoQ7GcBiK2qvLSa0gQLbBQJb73xXAhsCBQlYWHSAAL8JEGqvLSa0
+        gQLbtCAEGQEIAB0WIQQKSpZgG5Yt/OiY5oZDBckuYm60hQUCW+98VwAKCRBDBcku
+        Ym60hafuBACSkvwXAYfxvAf7IOK6+Sp3oWkrq6vsjH5K7oupTimR1cVgN1CEAWh8
+        2UBCg3zR5Q5BAnvnjeugdQVrAY+sftkaoy8qO5YYHCPtHtQKmXGWM7Q33hU1E7If
+        gU06qkFLhIOL3Vwr8jOuOzHv0M3PbLNr7lOCaIJ7uCjPBZuoqRwqjHt2BACuXwA8
+        RHbRGAxC65YgoSjGNu/da3q2J26E57KfSFprQ4TzAg33U4Wsqdx2vbbJxy1bfTYx
+        b0AYXe/+k23W7EIdBtMGOYwrX01oTfSIKbM+gDrHswSYXdOyziatLaUBSQfyG656
+        lGGZO/aArLZb4dgGeBHBwhXTufFDIYl3X94zfQ==
+        =RbPm
+        -----END PGP PRIVATE KEY BLOCK-----\
+        """);
   }
 }
diff --git a/java/com/google/gerrit/gpg/testing/TestTrustKeys.java b/java/com/google/gerrit/gpg/testing/TestTrustKeys.java
index 0f83f71..fb20aeb 100644
--- a/java/com/google/gerrit/gpg/testing/TestTrustKeys.java
+++ b/java/com/google/gerrit/gpg/testing/TestTrustKeys.java
@@ -43,105 +43,109 @@
    */
   public static TestKey keyA() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx6npoBCACp0vHePNPeLzm0HM35i70bRChyZXu/ARxOHZHNbh6hWJkq5saI\n"
-            + "zuzZoaqXAr3xZwHftTULlkgoJIt40x6VCT8EBnUHTkOqoHTsFnXg2kNuhFvmn0OX\n"
-            + "7RQFlR1SGoQG4fEy6t/GlOwEknpNSIMLkbDMP2FzEkLVWtlIe2hqqawVIgqzyO3k\n"
-            + "HaQxW8gWyyTx0qeSdSi4DyFZIzdyu/aZa7sj/MhO3DB3UwubW6yE+PMcAVrJD+0d\n"
-            + "EToMT7i8Erncc+xEzuXAoQUHaQfOXV4DG5qSgVpKaLxJ/ABWUri0eMPhj0cT4iDx\n"
-            + "eNTL7cZ4h72B1uJs8byDN74PHrypNiVE+IRHABEBAAG0HlRlc3R1c2VyIEEgPHRl\n"
-            + "c3RhQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqemgIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQ0lrQep/Q05ZxMAf+OoRzXWbGfv7kZb7xdrVyAUTAV4bU\n"
-            + "UvLoJZUIQ1ckPBcty2LUvY7l9efgp3c57nvTD6U98dVnsKfaW4PT0CRXlpl1IFyh\n"
-            + "kgbInFS5rO+cJMQn1KyC+FfiwyGNii630SwiHyWRG5+XQ6Iptx9JELwWUMCLJxFp\n"
-            + "B8DZQKlNnvdl+YUgEeQOkWTXfTSaBATdXHiZhskiumnTOGO24jSg8CrZc5O/n6fC\n"
-            + "CgEsAFWL7fnO0ii6EW1JH5btLHPxL9QI+5DJIypgOhGI1lqZW9KrpfmJ3w6N1Gek\n"
-            + "GBda98DmzxxxZ9iyq1cELAAiQMjkvws67cOs/hwXNn9YaK74dzhb49MLGIkBIAQQ\n"
-            + "AQIACgUCTHqf0QMFAXgACgkQV2Bph7AH1JCO/Qf+PBJqeWS7p32+K5r1cA7AeCB2\n"
-            + "pcHs78wLjnSxuimf0l+JItb9JQAKjzcdZTKVGkUivkq3zhsPCCtssgSav2wlG59F\n"
-            + "TaqtpGOxvGjc8TKWHW1TrPhV86wh0yUempKTMWfdZ0RAJVG3krAj60bzUsQNK41/\n"
-            + "0EZi4JI+sm/TRlwQcmEzdaGxhFSJqiJyaBWbPL8AQNA2iRyjMKNeGCrgapEl2IkW\n"
-            + "2ST+/yUPI/485LS0uU1+TLB+NhiJ6j5PoiVqYD+ul8WJ+cy1vvcp1GCQpbRv1yXY\n"
-            + "4GB1mw0JPIinVE1q+eKKQxN38zARPqyupiIuBQaqX9NCHCAdNtFc3kJQ7Nm83YkB\n"
-            + "IAQQAQIACgUCTHqkCwMFAXgACgkQZB8Rk9JP5GfGVQgArMBVQo3AD56p4g5A+DRA\n"
-            + "h0KdQMt4hs/dl+2GLAi+nK0wwuHrHvr9kcZNiQNMtu+YiwvxMpJ/JvXRwOp4wbEx\n"
-            + "6P6Uzp18R2sqbV4agnL5tXFZXfsa3OR2NLm56Ox1ReHnZtAcC6qa1nHqt9z2sTt1\n"
-            + "vh7IfK8GDU/3M3z4XBXPpmpZPAczqujuO/yshz84O6oc3noXfRUJRklbkhNC3WyS\n"
-            + "u5+3nupq4GwIYehQQpxBTD9xXj4hl3KfUnctg/MkgUGweEK3oZ22kObTLJttTP9t\n"
-            + "9q/hLkVyDtFhGorcsYbNZyupm3xhddzYovkReePwOO4WA7VeRqRdiYDU1UjIKvv4\n"
-            + "TrkBDQRMep6aAQgA3NQtBhS8yiEGN8rT4hGtuuprVd5jQVprLz4ImcI2+Gt71+CR\n"
-            + "gv/BZ0zzFp3VPjTGRusungJYkKKOGpEpERiqEG1X/ZyL7EzoyT+iKIMDsVJgmyDN\n"
-            + "cryHTejlKA8Z6GQ1hPlOIws22oLq5zQXxD9pzMDuabHl/s/bYlU5qXc7LhxdtrmT\n"
-            + "b2uBP9a+eneWKrz8OfgtS5m9DgqJ6Bjl0TvbeVJgKHX42pqzJlBTCn3hJjJosy8x\n"
-            + "4qTbqMraENnl9y+qynM7atoHX6TPWsD7vWtWvi+FA5OWGEe3rof8o/sJSj05DQUn\n"
-            + "i8mmSiCYW/tUklPPXOvPRP0GZ/GhBzIUtE3jBwARAQABiQEfBBgBAgAJBQJMep6a\n"
-            + "AhsMAAoJENJa0Hqf0NOW/gkH/jm9FL+S53NjrthdbNjffryhp7KhTmYAsRk3Hc3X\n"
-            + "4TBj3upecarJynpvsz5HlLi/OxDRR6L2yfjKk6/2iKAbV56mdnnu5xG3TG8++naL\n"
-            + "7n/s9TGBhgknb6+vGhSMZ/1dpQ6wkiyuEmgKJo8DzHAh3k3VATHiBeSD7fNSsgtK\n"
-            + "gzK0hi53IFRFDDPYiCca+SS6/pA2zF56JWGETiIa8rSHIQaK4hNJ38vgKOZM80vQ\n"
-            + "fp+CxvJkYY71Yc94oQByaQzrXod7xnukp5SXe/N3BYTFCWoaSTRUI/THRywWwKqa\n"
-            + "rUsttYrqs/EQSy0X3kZ7CAm04uzA8csNyxapEVRvJxbrt5I=\n"
-            + "=DAMW\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx6npoBCACp0vHePNPeLzm0HM35i70bRChyZXu/ARxOHZHNbh6hWJkq5saI\n"
-            + "zuzZoaqXAr3xZwHftTULlkgoJIt40x6VCT8EBnUHTkOqoHTsFnXg2kNuhFvmn0OX\n"
-            + "7RQFlR1SGoQG4fEy6t/GlOwEknpNSIMLkbDMP2FzEkLVWtlIe2hqqawVIgqzyO3k\n"
-            + "HaQxW8gWyyTx0qeSdSi4DyFZIzdyu/aZa7sj/MhO3DB3UwubW6yE+PMcAVrJD+0d\n"
-            + "EToMT7i8Erncc+xEzuXAoQUHaQfOXV4DG5qSgVpKaLxJ/ABWUri0eMPhj0cT4iDx\n"
-            + "eNTL7cZ4h72B1uJs8byDN74PHrypNiVE+IRHABEBAAEAB/9BbaG9Bz9zd0tqjrx2\n"
-            + "u/VQR3qz1FCQXtuqZu8RMC+B5zIf2si71clf8c7ZHnfSxWZt65Ez1SMYwDeyBdje\n"
-            + "/7B1Gw3Ekk00tFxHx0GEL2NSdZE4sbynkHIp0nD4/HlIc41rmh08E405F7wiAWFn\n"
-            + "uCpfDr47SNpR/A4BxHYOvi8r9pBxn/fXiHluqYROit0Z4tfKDCvQ47k+wqVD5nOt\n"
-            + "BEbHDfEwUMibgTuJ1qPyHf6HDlSdTQSfYV8QW1/UbHWus9QikfjGfLJpX0Rv3UG+\n"
-            + "WXHmowpRDVixj74UQCYXQ/AZi/OBlcS8PRY6EZV4RLyEWlZrdzKViNLOTUbJNHvA\n"
-            + "ZAQVBADQND7CIO6z4k8e9Z8Lf4iLWP9iIbH9R7ArTZr2mX1vkwp+sk0BNQurL/BQ\n"
-            + "jUHOJZnouwkc+C3pQi/JvGvAe1fLHPA0+NKe/tcuDXMk+L1HH6XmDgKtByac41AR\n"
-            + "txxqhaECNeK9OKXAXaEvenkGFMcqQV3QMiF2q5VlmFxSSXydEwQA0M8tCowz0iZF\n"
-            + "i3fGuuZDTN3Ut4u6Uf9FiLcR4ye2Aa5ppO8vlNjObNqpHz0UqdDjB+e3O/n7BUx3\n"
-            + "A5PRZNQvcMbhgr2U3zjWvFMHS3YuxbuIaZ1Vj69vpOAGkUc98v4i0/3Lk7Lijpto\n"
-            + "n40S0eCVo+eccHA4HRvS5XSdNGHVJn0EAMzfBt3DalOlHm+PrAiZdVdp5IfbJwJv\n"
-            + "xkyI++0p4VaYTZhOxjswTs6vgv30FBmHAlx1FzoUOKLaOhxPyLgamFd9YG+ab4DK\n"
-            + "chc4TxIj3kkx3/m6JufW8DWhKyAJNZ/MW+Iqop5pUIeTbOBlNyaflK+XxjkP71rP\n"
-            + "2gZx4pjYjK5EPDy0HlRlc3R1c2VyIEEgPHRlc3RhQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTHqemgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ0lrQep/Q\n"
-            + "05ZxMAf+OoRzXWbGfv7kZb7xdrVyAUTAV4bUUvLoJZUIQ1ckPBcty2LUvY7l9efg\n"
-            + "p3c57nvTD6U98dVnsKfaW4PT0CRXlpl1IFyhkgbInFS5rO+cJMQn1KyC+FfiwyGN\n"
-            + "ii630SwiHyWRG5+XQ6Iptx9JELwWUMCLJxFpB8DZQKlNnvdl+YUgEeQOkWTXfTSa\n"
-            + "BATdXHiZhskiumnTOGO24jSg8CrZc5O/n6fCCgEsAFWL7fnO0ii6EW1JH5btLHPx\n"
-            + "L9QI+5DJIypgOhGI1lqZW9KrpfmJ3w6N1GekGBda98DmzxxxZ9iyq1cELAAiQMjk\n"
-            + "vws67cOs/hwXNn9YaK74dzhb49MLGJ0DmARMep6aAQgA3NQtBhS8yiEGN8rT4hGt\n"
-            + "uuprVd5jQVprLz4ImcI2+Gt71+CRgv/BZ0zzFp3VPjTGRusungJYkKKOGpEpERiq\n"
-            + "EG1X/ZyL7EzoyT+iKIMDsVJgmyDNcryHTejlKA8Z6GQ1hPlOIws22oLq5zQXxD9p\n"
-            + "zMDuabHl/s/bYlU5qXc7LhxdtrmTb2uBP9a+eneWKrz8OfgtS5m9DgqJ6Bjl0Tvb\n"
-            + "eVJgKHX42pqzJlBTCn3hJjJosy8x4qTbqMraENnl9y+qynM7atoHX6TPWsD7vWtW\n"
-            + "vi+FA5OWGEe3rof8o/sJSj05DQUni8mmSiCYW/tUklPPXOvPRP0GZ/GhBzIUtE3j\n"
-            + "BwARAQABAAf+KQOPSS3Y0oHHsd0N9VLrPWgEf3JKZPzyI1gWKNiVdRYhbjrbS8VM\n"
-            + "mm8ERxMRY/hRSyKrCdXNtS87zVtgkThPfbWRPh0xL7YpFhenena63Ng78RPqlIDH\n"
-            + "cITs6r/DRBI4jnXvOTr/+R2Pm1llgKF2ePzsSt0rpmPcjyrdBsiKSUnLGxm4tGtW\n"
-            + "wVoEjy3+MRN2ULyTO8Pe4URKTtUkkb23iuQuJZy+k+SfH+H0/3oEb8ERRE3UXNG7\n"
-            + "BIbaj71nsx8+H8+x8ffRm1s5Unn86AJ418oEhxNzQk59NnrrlJ4HH9NNbjjzI3JE\n"
-            + "intSQKhFJsvMARdzX062yartQtnm1v6jwQQA65rpMMHCoh9pxvL6yagw3WjQLEPw\n"
-            + "vOGpD9ossBvcv/SfAe7SgJsx6J6X0IIW6EKIjyRhWTIfK/rVR0cmUFTGStib+y22\n"
-            + "BPcQmt/Oiw9rdUfOmDrnosPC0SB+19tKw1v1AfW5swpJnGBCkGz9UfX4Fr/eTS3e\n"
-            + "2KaMq+r1KALSUVkEAO/x0SWOiBRH3X1ETNE9nLTP6u2W3TAvrd+dXyP7JjXWZPB8\n"
-            + "NOwT7qidvUlhTbxdR7xWNI1W924Ywwgs43cAPGyq95pjdzhvi0Xxab7124UK+MS3\n"
-            + "V4WBvjOYYW8pkdMOydRLETXSkco2mDCRTiVKe3Zi7p+lKlVJj4xrFUPUnetfBADH\n"
-            + "EPwYeeZ8sQnW644J75eoph2e5KLRJaOy5GMPRLNmq+ODtJxdoIGpfQnEA35nSlze\n"
-            + "Ea+1UvLBlWyF+p08bNfnXHp3j5ugucAYbVEs4ptUwTB3vFt7eJ8rkx9GYcuBFiwm\n"
-            + "H47rg7QmS1mWDLyX6v2pI9brsb1SCgBL+oi9CyjypkjqiQEfBBgBAgAJBQJMep6a\n"
-            + "AhsMAAoJENJa0Hqf0NOW/gkH/jm9FL+S53NjrthdbNjffryhp7KhTmYAsRk3Hc3X\n"
-            + "4TBj3upecarJynpvsz5HlLi/OxDRR6L2yfjKk6/2iKAbV56mdnnu5xG3TG8++naL\n"
-            + "7n/s9TGBhgknb6+vGhSMZ/1dpQ6wkiyuEmgKJo8DzHAh3k3VATHiBeSD7fNSsgtK\n"
-            + "gzK0hi53IFRFDDPYiCca+SS6/pA2zF56JWGETiIa8rSHIQaK4hNJ38vgKOZM80vQ\n"
-            + "fp+CxvJkYY71Yc94oQByaQzrXod7xnukp5SXe/N3BYTFCWoaSTRUI/THRywWwKqa\n"
-            + "rUsttYrqs/EQSy0X3kZ7CAm04uzA8csNyxapEVRvJxbrt5I=\n"
-            + "=FLdD\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =DAMW
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        lQOYBEx6npoBCACp0vHePNPeLzm0HM35i70bRChyZXu/ARxOHZHNbh6hWJkq5saI
+        zuzZoaqXAr3xZwHftTULlkgoJIt40x6VCT8EBnUHTkOqoHTsFnXg2kNuhFvmn0OX
+        7RQFlR1SGoQG4fEy6t/GlOwEknpNSIMLkbDMP2FzEkLVWtlIe2hqqawVIgqzyO3k
+        HaQxW8gWyyTx0qeSdSi4DyFZIzdyu/aZa7sj/MhO3DB3UwubW6yE+PMcAVrJD+0d
+        EToMT7i8Erncc+xEzuXAoQUHaQfOXV4DG5qSgVpKaLxJ/ABWUri0eMPhj0cT4iDx
+        eNTL7cZ4h72B1uJs8byDN74PHrypNiVE+IRHABEBAAEAB/9BbaG9Bz9zd0tqjrx2
+        u/VQR3qz1FCQXtuqZu8RMC+B5zIf2si71clf8c7ZHnfSxWZt65Ez1SMYwDeyBdje
+        /7B1Gw3Ekk00tFxHx0GEL2NSdZE4sbynkHIp0nD4/HlIc41rmh08E405F7wiAWFn
+        uCpfDr47SNpR/A4BxHYOvi8r9pBxn/fXiHluqYROit0Z4tfKDCvQ47k+wqVD5nOt
+        BEbHDfEwUMibgTuJ1qPyHf6HDlSdTQSfYV8QW1/UbHWus9QikfjGfLJpX0Rv3UG+
+        WXHmowpRDVixj74UQCYXQ/AZi/OBlcS8PRY6EZV4RLyEWlZrdzKViNLOTUbJNHvA
+        ZAQVBADQND7CIO6z4k8e9Z8Lf4iLWP9iIbH9R7ArTZr2mX1vkwp+sk0BNQurL/BQ
+        jUHOJZnouwkc+C3pQi/JvGvAe1fLHPA0+NKe/tcuDXMk+L1HH6XmDgKtByac41AR
+        txxqhaECNeK9OKXAXaEvenkGFMcqQV3QMiF2q5VlmFxSSXydEwQA0M8tCowz0iZF
+        i3fGuuZDTN3Ut4u6Uf9FiLcR4ye2Aa5ppO8vlNjObNqpHz0UqdDjB+e3O/n7BUx3
+        A5PRZNQvcMbhgr2U3zjWvFMHS3YuxbuIaZ1Vj69vpOAGkUc98v4i0/3Lk7Lijpto
+        n40S0eCVo+eccHA4HRvS5XSdNGHVJn0EAMzfBt3DalOlHm+PrAiZdVdp5IfbJwJv
+        xkyI++0p4VaYTZhOxjswTs6vgv30FBmHAlx1FzoUOKLaOhxPyLgamFd9YG+ab4DK
+        chc4TxIj3kkx3/m6JufW8DWhKyAJNZ/MW+Iqop5pUIeTbOBlNyaflK+XxjkP71rP
+        2gZx4pjYjK5EPDy0HlRlc3R1c2VyIEEgPHRlc3RhQGV4YW1wbGUuY29tPokBOAQT
+        AQIAIgUCTHqemgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ0lrQep/Q
+        05ZxMAf+OoRzXWbGfv7kZb7xdrVyAUTAV4bUUvLoJZUIQ1ckPBcty2LUvY7l9efg
+        p3c57nvTD6U98dVnsKfaW4PT0CRXlpl1IFyhkgbInFS5rO+cJMQn1KyC+FfiwyGN
+        ii630SwiHyWRG5+XQ6Iptx9JELwWUMCLJxFpB8DZQKlNnvdl+YUgEeQOkWTXfTSa
+        BATdXHiZhskiumnTOGO24jSg8CrZc5O/n6fCCgEsAFWL7fnO0ii6EW1JH5btLHPx
+        L9QI+5DJIypgOhGI1lqZW9KrpfmJ3w6N1GekGBda98DmzxxxZ9iyq1cELAAiQMjk
+        vws67cOs/hwXNn9YaK74dzhb49MLGJ0DmARMep6aAQgA3NQtBhS8yiEGN8rT4hGt
+        uuprVd5jQVprLz4ImcI2+Gt71+CRgv/BZ0zzFp3VPjTGRusungJYkKKOGpEpERiq
+        EG1X/ZyL7EzoyT+iKIMDsVJgmyDNcryHTejlKA8Z6GQ1hPlOIws22oLq5zQXxD9p
+        zMDuabHl/s/bYlU5qXc7LhxdtrmTb2uBP9a+eneWKrz8OfgtS5m9DgqJ6Bjl0Tvb
+        eVJgKHX42pqzJlBTCn3hJjJosy8x4qTbqMraENnl9y+qynM7atoHX6TPWsD7vWtW
+        vi+FA5OWGEe3rof8o/sJSj05DQUni8mmSiCYW/tUklPPXOvPRP0GZ/GhBzIUtE3j
+        BwARAQABAAf+KQOPSS3Y0oHHsd0N9VLrPWgEf3JKZPzyI1gWKNiVdRYhbjrbS8VM
+        mm8ERxMRY/hRSyKrCdXNtS87zVtgkThPfbWRPh0xL7YpFhenena63Ng78RPqlIDH
+        cITs6r/DRBI4jnXvOTr/+R2Pm1llgKF2ePzsSt0rpmPcjyrdBsiKSUnLGxm4tGtW
+        wVoEjy3+MRN2ULyTO8Pe4URKTtUkkb23iuQuJZy+k+SfH+H0/3oEb8ERRE3UXNG7
+        BIbaj71nsx8+H8+x8ffRm1s5Unn86AJ418oEhxNzQk59NnrrlJ4HH9NNbjjzI3JE
+        intSQKhFJsvMARdzX062yartQtnm1v6jwQQA65rpMMHCoh9pxvL6yagw3WjQLEPw
+        vOGpD9ossBvcv/SfAe7SgJsx6J6X0IIW6EKIjyRhWTIfK/rVR0cmUFTGStib+y22
+        BPcQmt/Oiw9rdUfOmDrnosPC0SB+19tKw1v1AfW5swpJnGBCkGz9UfX4Fr/eTS3e
+        2KaMq+r1KALSUVkEAO/x0SWOiBRH3X1ETNE9nLTP6u2W3TAvrd+dXyP7JjXWZPB8
+        NOwT7qidvUlhTbxdR7xWNI1W924Ywwgs43cAPGyq95pjdzhvi0Xxab7124UK+MS3
+        V4WBvjOYYW8pkdMOydRLETXSkco2mDCRTiVKe3Zi7p+lKlVJj4xrFUPUnetfBADH
+        EPwYeeZ8sQnW644J75eoph2e5KLRJaOy5GMPRLNmq+ODtJxdoIGpfQnEA35nSlze
+        Ea+1UvLBlWyF+p08bNfnXHp3j5ugucAYbVEs4ptUwTB3vFt7eJ8rkx9GYcuBFiwm
+        H47rg7QmS1mWDLyX6v2pI9brsb1SCgBL+oi9CyjypkjqiQEfBBgBAgAJBQJMep6a
+        AhsMAAoJENJa0Hqf0NOW/gkH/jm9FL+S53NjrthdbNjffryhp7KhTmYAsRk3Hc3X
+        4TBj3upecarJynpvsz5HlLi/OxDRR6L2yfjKk6/2iKAbV56mdnnu5xG3TG8++naL
+        7n/s9TGBhgknb6+vGhSMZ/1dpQ6wkiyuEmgKJo8DzHAh3k3VATHiBeSD7fNSsgtK
+        gzK0hi53IFRFDDPYiCca+SS6/pA2zF56JWGETiIa8rSHIQaK4hNJ38vgKOZM80vQ
+        fp+CxvJkYY71Yc94oQByaQzrXod7xnukp5SXe/N3BYTFCWoaSTRUI/THRywWwKqa
+        rUsttYrqs/EQSy0X3kZ7CAm04uzA8csNyxapEVRvJxbrt5I=
+        =FLdD
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -150,93 +154,97 @@
    */
   public static TestKey keyB() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0\n"
-            + "PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF\n"
-            + "UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM\n"
-            + "mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY\n"
-            + "75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ\n"
-            + "jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAG0HlRlc3R1c2VyIEIgPHRl\n"
-            + "c3RiQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIG\n"
-            + "FQgCCQoLBBYCAwECHgECF4AACgkQV2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6et\n"
-            + "H6NYWDUeAKXe9mfXBJ39HdtlF50jZ5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscva\n"
-            + "RiTtt+KUxDZSYbEHrC0EO7w0Wi5ltwaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhm\n"
-            + "AqC/6kgHuXeY/7EAzwU3o0wKbmfx1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoS\n"
-            + "JB5+lKajtIE6kMn9m8CWM66/zxSCY3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2I\n"
-            + "IjM5RHQ9hTsR7NQ9JUTFmpKZlcdah93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHp\n"
-            + "Q7kBDQRMep7TAQgAwOuLBXnACIsd879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDw\n"
-            + "LxL4uVh3q/ksESHnQPPqxFYkgeA66SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g\n"
-            + "5iw5hH+2ZWrGlu3P65UdQUJW+JaDx1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JL\n"
-            + "Ed+6OIwWblU7ZogfiNpgZJ0lapxTe84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ\n"
-            + "0ZD5i9s1MAxdw4OD+705owPCQnqsr18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlK\n"
-            + "wHSRtHLLJoowJ5fXw5UbZcUtRUergxFRwae87wARAQABiQElBBgBAgAPBQJMep7T\n"
-            + "AhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec/v9uEvYQ\n"
-            + "XqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkjKeR9dXXe\n"
-            + "UzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZiWRdh+8W\n"
-            + "0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeuhQqdCULQ\n"
-            + "ZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97l6DQ//H7\n"
-            + "wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=\n"
-            + "=tmW1\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0\n"
-            + "PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF\n"
-            + "UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM\n"
-            + "mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY\n"
-            + "75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ\n"
-            + "jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAEAB/wPPV1Om92pc9F3jJsZ\n"
-            + "2F3YZxukLfjnA76tnMEWd/pYGrUhdV3AdY4r/aB0njSeApxdXRlLQ3L2cUxdGCJQ\n"
-            + "mzM1ies7IXCC/w5WaShwAG+zpmFL/5+cq3vDc9tb2Q/IasVOVFQYEE2el7SfW5Cp\n"
-            + "mjZFGR8V1wvdNvC0Q0IHrmfdECYSeftzZBEj7CcoGc2pF5zpCG0XQxq7K6cEeSf5\n"
-            + "TKf//UVHgyBCIso6mzgP5k6DGw2d64843CPhhlHEbirUu/wNnbm1SqJ5xFL2VatH\n"
-            + "w7ij4V/hbgnP0GQkbY5+p/PU74P7fx/Ee8D8mF2HmEKRy6ZQY/SAnrjsAURBYR5S\n"
-            + "GF5RBADfhOYEgseWr81lq6Y1oM4YQz+pXRIZk34BagOJsL767B7+uwhvmxBJKIOS\n"
-            + "nRIxfV8GlvT22hrbqsRRyusoIlo2ZUat94IMAL6Oqm6VFm71PT3z9+ukWK43FIXf\n"
-            + "Bsz4swSV001398e3jpSizI6fGW7LRxvnua+NPN+xJLmDVcsPvwQA49ajm48NorD9\n"
-            + "bIWG87+2ScNTVOnHKryR+/LrGWA0f3G6LUsHZPKHNBdFZ4yza2QtEKw95L3K9D4y\n"
-            + "jIeKGwSRYJPb5oh5tSge58pxwP88eI9J4dL+XF1nsG0vYF9B41+qG1TCsPyUJTp6\n"
-            + "ry7NAgWrbpsZpjB0yJ1kFva3iS/hD00EAMu66p1CtsosoDHhekvRZp8a3svd+8uf\n"
-            + "YEKkEKXZuNNmJJktJBSA2FK1RKl9bV8wuG0Pi1/k39egLO3QTjruWUbSggT+aibR\n"
-            + "RW3hU7G+Z5IBOU3p+kTFLat6+TBg0XhCjJ+Eq366nZy1QIfqTCixIaDwrutZd6DC\n"
-            + "BXOjdoG6ZvLcQia0HlRlc3R1c2VyIEIgPHRlc3RiQGV4YW1wbGUuY29tPokBPgQT\n"
-            + "AQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ\n"
-            + "V2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6etH6NYWDUeAKXe9mfXBJ39HdtlF50j\n"
-            + "Z5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscvaRiTtt+KUxDZSYbEHrC0EO7w0Wi5l\n"
-            + "twaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhmAqC/6kgHuXeY/7EAzwU3o0wKbmfx\n"
-            + "1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoSJB5+lKajtIE6kMn9m8CWM66/zxSC\n"
-            + "Y3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2IIjM5RHQ9hTsR7NQ9JUTFmpKZlcda\n"
-            + "h93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHpQ50DmARMep7TAQgAwOuLBXnACIsd\n"
-            + "879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDwLxL4uVh3q/ksESHnQPPqxFYkgeA6\n"
-            + "6SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g5iw5hH+2ZWrGlu3P65UdQUJW+JaD\n"
-            + "x1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JLEd+6OIwWblU7ZogfiNpgZJ0lapxT\n"
-            + "e84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ0ZD5i9s1MAxdw4OD+705owPCQnqs\n"
-            + "r18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlKwHSRtHLLJoowJ5fXw5UbZcUtRUer\n"
-            + "gxFRwae87wARAQABAAf8DAVBKsyswfuFGMB2vpSiVxaEnV3/2LoHFOOb45XwJSqV\n"
-            + "HL3+mThJ5iaUglMqw0CFC7+HA8fIS41grlFSDgNC02OcjS9rUxDg0En/pp17Gks0\n"
-            + "D+D7bSwZQ1+/yi7ug836lBe89GmBSMj8GgnK9T6RBGOL8nZ72b2ftK4CNWMmAfo4\n"
-            + "NZUy+rnnziV5WoYrkFZhl3dMMd3nITILBy9eYUoiKJl8O1b8amhrNkB/PEMAV7jc\n"
-            + "260XEQ9fgzMMe5/oT8pzIOGyrB+QO5rMu9pGVJ1qeMzTiZjjHXE2CEaEbvEk0F4l\n"
-            + "6w2gp5C6O5GoMpCOPwCy7dOYX5ETdO4Ppjnrob2XEQQAwus5q+EFoBVG8vfEf56x\n"
-            + "czkC15+0VcMe/IM8l/ur/oF1NUlAnPCq7WfgdELvGNszW7R+A625yXJJf7LJE/y/\n"
-            + "5GUGHAK60FUa0ElbVEn0A6kDcvll0dM6rKPQvFguaFpBKXre6k17cdOrf9hasfJk\n"
-            + "+lzaHlh9hJgoM30pAwG4+n8EAP1f+TEkEfVFo4Uy84eO6xVkYVndopDU1gCpfW1a\n"
-            + "84SA2PNjU3vkdIoFsEvOmf1xlfYeDYn37dikFPEZDsHBUzELDMewAXRgmVvnMJrj\n"
-            + "8Zq4FbEQSVjyz3qJOGk5V999qqoVMRXdnlQs5IXgZauPsnIqi5TRQZOMhbaiOVBO\n"
-            + "kqWRBAC9FhxypA3t9j1zGTFDppWmcBxpVzGGsgmzGO+WTVyk6szbZgTsf2+R+gTJ\n"
-            + "ZKVVzE6Mu+iZmPbrn/x7LWzKJuavRz0xSrvCYbIxYyheFz5LOPFHLF181h1g79gY\n"
-            + "E5Tz7uwu3jIldM7rY5RhxS6V5GGDVSfA+/Dsk6Iaujs6Hs7y30C0iQElBBgBAgAP\n"
-            + "BQJMep7TAhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec\n"
-            + "/v9uEvYQXqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkj\n"
-            + "KeR9dXXeUzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZ\n"
-            + "iWRdh+8W0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeu\n"
-            + "hQqdCULQZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97\n"
-            + "l6DQ//H7wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=\n"
-            + "=uFLT\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0
+        PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF
+        UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM
+        mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY
+        75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ
+        jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAG0HlRlc3R1c2VyIEIgPHRl
+        c3RiQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIG
+        FQgCCQoLBBYCAwECHgECF4AACgkQV2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6et
+        H6NYWDUeAKXe9mfXBJ39HdtlF50jZ5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscva
+        RiTtt+KUxDZSYbEHrC0EO7w0Wi5ltwaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhm
+        AqC/6kgHuXeY/7EAzwU3o0wKbmfx1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoS
+        JB5+lKajtIE6kMn9m8CWM66/zxSCY3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2I
+        IjM5RHQ9hTsR7NQ9JUTFmpKZlcdah93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHp
+        Q7kBDQRMep7TAQgAwOuLBXnACIsd879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDw
+        LxL4uVh3q/ksESHnQPPqxFYkgeA66SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g
+        5iw5hH+2ZWrGlu3P65UdQUJW+JaDx1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JL
+        Ed+6OIwWblU7ZogfiNpgZJ0lapxTe84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ
+        0ZD5i9s1MAxdw4OD+705owPCQnqsr18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlK
+        wHSRtHLLJoowJ5fXw5UbZcUtRUergxFRwae87wARAQABiQElBBgBAgAPBQJMep7T
+        AhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec/v9uEvYQ
+        XqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkjKeR9dXXe
+        UzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZiWRdh+8W
+        0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeuhQqdCULQ
+        ZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97l6DQ//H7
+        wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=
+        =tmW1
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =uFLT
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -245,105 +253,109 @@
    */
   public static TestKey keyC() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx6nuMBCADd077pyfsDGbGhHh+7xzipWihMJRrzQnpbSeVJIxA/Js+Z2MW8\n"
-            + "9J98AgnjONjGVlLqtp11O8Bp9xgdoGYWvFl2CrooQrCe+70JORHE30MJT+61mgLQ\n"
-            + "jm9l2WmIIcuzNwoTOKqWlXuaRIKddXMVbwr++Enl/9znx81FCf1KioDijeeHzVZb\n"
-            + "IjELLCtLlhwhGlYNy6LfNhSY+rNHOomIM9CUXkGZU7JvTe3M1plUzYYIFu3tttZI\n"
-            + "b6e1FSfR60yZ/f88fLacloc3fSrPWA261R/gHuFfLCdTt/I3EcYE+x33LZnSSOgz\n"
-            + "v/JtAuFlCaF/oNRTJHeRbALeri+FxBYule15ABEBAAG0HlRlc3R1c2VyIEMgPHRl\n"
-            + "c3RjQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqe4wIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQZB8Rk9JP5GcEIgf/cMvYBwH8ENrWec366Txaaeh/TO6n\n"
-            + "4v4P2LUR4/hcrNpHx3+9ikznkyF/b8OCsOE+KstvOO6i9vuRGVBPmfoALVv8iCGs\n"
-            + "5MXZJskjACXOqQav0I7ZY5rDJxuOKq6DrxtpHNxK8n0D1PEZllyk/OZVBAcjL2vu\n"
-            + "WC6ujP3jbMKaV0+heFqOVIghQjdA4McLH2u1XLOGEZdp7hLfmTnClmfzbnslFBSQ\n"
-            + "xU2g3jCq2k2zAPhn+jOGCL0987QGj1e6pHRXdUxcfnLRyNadRied0HO/clIb8vdt\n"
-            + "UaexujHjgg+1KDxj4PBAftN2lRtnnsSG9z4T31aTFz5YVG+pq8UXk9ohCokBIAQQ\n"
-            + "AQIACgUCTHqkKQMFAngACgkQqZHi1Q/dNnexiQf/ba9LcR76+tVvos1cxrGO3VkD\n"
-            + "3R1pvIWsb37/NTypWCvrFhsy4OUEy3bVCfJcqfwdY3Q2XixB9kuKo3qCSom1EjGg\n"
-            + "Qhr5ZsrB3qYqaa6S0AeVusmIwArEr9uuMUDjXhKlUALDX8HfXWGy2UmjNJkkT8Jm\n"
-            + "GtISS4KOfXUuZY04DttvbukEnyxAiLU9V0BnzrI9DARh0gEjqjUZAVyP5lOXJJxt\n"
-            + "sau95mOe8E61GELXPkxDLrnCboX7ys2OxcFO6S7q1xJPkki2SVq0y0k5oY/3jktw\n"
-            + "jO8uC3n7NiyW+BYJK6+zj3u3iA+o0YGm+i6F7aneJEaJrFqRj9L1vbojvuH0cYkB\n"
-            + "IAQQAQIACgUCTHqkOwMFAngACgkQOwm5f0tDh+7dSQf+PnEUftNSOuLVLoJ+2tyD\n"
-            + "DPJpcLIavNCyNR3hCGL86NXRUxOrmYgDVVv8pJuYB6aUTm69rFFZlzNwqQN5pBiX\n"
-            + "Zr3NM1jgJT6gKfXddcg1p/X2S9+xn4RN92R0fn0kEjM65fpE1Do+YWHOuHDZEOrx\n"
-            + "L8OaSo8lr19+r27fn09/HBhz2lOyTYzsdTjHeWdxPVQ3JNiVX11k7iKsttdYtM/V\n"
-            + "mAHzzd54Kvt5So/2qLIAcfSmUe9DQAdmcEcJQpQ2veND9uwccX7tH0cH4n9Cp16o\n"
-            + "quJ2pxWzOvKR3zxSw+cRxyIS4VjT6k+UsG3Lw55QZgdb5IEaJfezPj+tOhQlQz0f\n"
-            + "VrkBDQRMep7jAQgAw+67ahlOGnkF6mTtmg6MOGzAbRQ11MNrORnNtGOccNgtlgrO\n"
-            + "Y8TBqw1HkJ56v26E1FxfRh69CUGkYVXx0tMw0QbI+unX35ce5hJD4aWa8bOA1vfw\n"
-            + "474p/NpI+czWsFvcdOu5K6xIGXHShaQQyf2FQ9QeIFrU60qfaBL5jzuLyujCACqU\n"
-            + "46QGgBgeUjaT54LjrWSdn/Jtsbpv0MPv3Ea8fMdtSMkkBsDkF55jaJDFYq+xbs+e\n"
-            + "IKBjTwtSvrUisnLAC0Z9YY21GXGI3DGYqpVXz+Fe5xMTX1a6K3VKEmxmX2m/ebhm\n"
-            + "1p6EqjAJguOjJbJJQHKHMOol0zU6ANB6SgP26wARAQABiQEfBBgBAgAJBQJMep7j\n"
-            + "AhsMAAoJEGQfEZPST+Rn7AcH/32HACPLdxINsi8OSWa8OccMG5XEUvHTZjmdwVT2\n"
-            + "czMss8nwgifU9D4hEVRu1MWpiyxUgegW94wuSh4PWIVOVd18PmzAYc73aYgonakb\n"
-            + "M+MDIqGVvAH8QtHo79sqZ9vrkQaQXB3Y8cq+WxDQZyl8KLXP2icmq1Rl6Q6+i9oS\n"
-            + "pFe88Wr0cGaTblkfDbbWcih3C6tKAfcFwLLg8u4jYfXjZg/E9eAJf0dIFcQSQoHd\n"
-            + "O8hVXaZwx/rYXA8UFwAuROo2nu6SIof1lrH92p+now95d5zUZ5BYnKVd3uXsln0j\n"
-            + "z585UPQKS2J8PUy9IirmahgTyEYFwO64kZ2B4hYOE2g+rYw=\n"
-            + "=LtMR\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx6nuMBCADd077pyfsDGbGhHh+7xzipWihMJRrzQnpbSeVJIxA/Js+Z2MW8\n"
-            + "9J98AgnjONjGVlLqtp11O8Bp9xgdoGYWvFl2CrooQrCe+70JORHE30MJT+61mgLQ\n"
-            + "jm9l2WmIIcuzNwoTOKqWlXuaRIKddXMVbwr++Enl/9znx81FCf1KioDijeeHzVZb\n"
-            + "IjELLCtLlhwhGlYNy6LfNhSY+rNHOomIM9CUXkGZU7JvTe3M1plUzYYIFu3tttZI\n"
-            + "b6e1FSfR60yZ/f88fLacloc3fSrPWA261R/gHuFfLCdTt/I3EcYE+x33LZnSSOgz\n"
-            + "v/JtAuFlCaF/oNRTJHeRbALeri+FxBYule15ABEBAAEAB/sFPLoJDG1eV5QpqEZf\n"
-            + "m/QMOTOn8ZJ9xraQvXFvV7zgVXxJBvTLMbuACrnHnoiCrULS+w8Dt66Nfz7s4yQJ\n"
-            + "5SDtFX2AlMDVWL7wBEPgF1UpN6ox1CzSa6HOaygaUFGeKHO20WDjV4HmBLhQkKIa\n"
-            + "vKbghHA/4Nm1s1z3BHB8GtdGZ1VHc+s1DhPK5w+WHqYpLYjpNmI9yJg3gclEqEG9\n"
-            + "XzBqTZm9mPJRBdDMOD0xLa4nUD3Dkrjimqod3X7EuXE6sT2DuGVa1nuynk/8gIyO\n"
-            + "uS6crY7YJzEQUtQJ2n3y/h+QnZFo9UFuIVpgsxhBDsCnYNFWNR91Q0IM6PohHvqx\n"
-            + "BtFhBADsax1Bc0obP+bIkeAXltGlUYqm3bjOgVZ87XR0qe4TGwXGe8T1Yjfc8rj0\n"
-            + "cfBYCud201r/05CgchojMnTWlFLg308bSIZ9YvN3oOVay8nZ7h62dUIs45zebw3R\n"
-            + "SHwvjE5Sm/VWIdLrUUW1aGfk/VPudNMMMu2C64ev8DF/iwYjoQQA8DM+9oPvFJPA\n"
-            + "kLYg71tP2iIE5GbFqkiIEx59eQUxTsn6ubEfREjI99QliAdcKbyRHc3jc68NopLB\n"
-            + "41L7ny0j6VKuEszOYhhQ0qQK/jlI461aG14qHAylhuQTLrjpsUPE+WelBm9bxli0\n"
-            + "gA8F81WLOvJ2HzuMYVrj3tjGl3AHetkEAI77VKxGCGRzK63qBnmLwQEvqbphpgxH\n"
-            + "ANNAsg5HuWtDUgk85t2nrIgL1kfhu++CfP9duN/qU4dw/bgJaKOamWTfLBwST8qe\n"
-            + "3F8omovi1vLzHVpmvQp6Ly4wggJ4Gl/n0DNFopKw20V8ZTiRYtuLS43H7VsczE+8\n"
-            + "NKjy01EgHDMAP8O0HlRlc3R1c2VyIEMgPHRlc3RjQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTHqe4wIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQZB8Rk9JP\n"
-            + "5GcEIgf/cMvYBwH8ENrWec366Txaaeh/TO6n4v4P2LUR4/hcrNpHx3+9ikznkyF/\n"
-            + "b8OCsOE+KstvOO6i9vuRGVBPmfoALVv8iCGs5MXZJskjACXOqQav0I7ZY5rDJxuO\n"
-            + "Kq6DrxtpHNxK8n0D1PEZllyk/OZVBAcjL2vuWC6ujP3jbMKaV0+heFqOVIghQjdA\n"
-            + "4McLH2u1XLOGEZdp7hLfmTnClmfzbnslFBSQxU2g3jCq2k2zAPhn+jOGCL0987QG\n"
-            + "j1e6pHRXdUxcfnLRyNadRied0HO/clIb8vdtUaexujHjgg+1KDxj4PBAftN2lRtn\n"
-            + "nsSG9z4T31aTFz5YVG+pq8UXk9ohCp0DmARMep7jAQgAw+67ahlOGnkF6mTtmg6M\n"
-            + "OGzAbRQ11MNrORnNtGOccNgtlgrOY8TBqw1HkJ56v26E1FxfRh69CUGkYVXx0tMw\n"
-            + "0QbI+unX35ce5hJD4aWa8bOA1vfw474p/NpI+czWsFvcdOu5K6xIGXHShaQQyf2F\n"
-            + "Q9QeIFrU60qfaBL5jzuLyujCACqU46QGgBgeUjaT54LjrWSdn/Jtsbpv0MPv3Ea8\n"
-            + "fMdtSMkkBsDkF55jaJDFYq+xbs+eIKBjTwtSvrUisnLAC0Z9YY21GXGI3DGYqpVX\n"
-            + "z+Fe5xMTX1a6K3VKEmxmX2m/ebhm1p6EqjAJguOjJbJJQHKHMOol0zU6ANB6SgP2\n"
-            + "6wARAQABAAf9HIsMy8S/92SmE018vQgILrgjwursz1Vgq22HkBNALm2acSnwgzbz\n"
-            + "V8M+0mH5U9ClPSKae+aXzLS+s7IHi++u7uSO0YQmKgZ5PonD+ygFoyxumo0oOfqc\n"
-            + "DJ/oKFaforWJ2jv05S3bRbRVN5l9G0/5jWC7ZXnrXBOqQUkdCLFjXhMPq3zg2Yy3\n"
-            + "XSU83dVteOtrYRZqv33umZNCdk44z6kQOvh9tgSCL/aZ3d7AqjRK99I/IYY1IuVN\n"
-            + "qreFriVcJ0EzlnbPCnva+ReWAd2zt5VEClGu9J0CVnHmZNlwfmbFSiUN1hiMonkr\n"
-            + "sFImlw3adfJ7dsi/GzCC4147ep6jXw7QwQQAzwkeRWR9xc3ndrnXqUbQmgQkAD3D\n"
-            + "p2cwPygyLr0UDBDVX0z+8GKeBhNs3KIFXwUs6GxmDodHh0t4HUJeVLs7ur5ZATqo\n"
-            + "Bx50cSUOoaeSHRFVwicdJRtVgTTQ4UwwmKcLLJe2fWv6hnmyInK7Lp8ThLGQgqo8\n"
-            + "UWg3cdfzCvhKSvsEAPJFYhsFA/E92xUpzP8oYs3AA4mUXB+F0eObe9gqv8lAE6SX\n"
-            + "gB5kWhcd+MGddUGJuJV2LRrgOx3nXu3m3n35AH6iAY4Qi9URPzi/K659oefUU1c5\n"
-            + "BFArHX9bN1k1cOvH28tpQ38eAxaMygLqyR5Q5VbtZ5tYqLKCvHVs3I8lekDRA/4i\n"
-            + "e0vlu34qenppPANPm+Vq/7cSlG3XY4ioxwC/j6Y+92u90DXbbGatOg1SqGSwn1VP\n"
-            + "S034m7bDCNoWOXL0yAcbXrLZV74AyfvVOYOs/WtehehzWeTQRT5lkxX5+xGc1/h6\n"
-            + "9HQvsKKnUK8n1oc5aM5xzRVkU9+kcmqYqXqyOHnIbDbPiQEfBBgBAgAJBQJMep7j\n"
-            + "AhsMAAoJEGQfEZPST+Rn7AcH/32HACPLdxINsi8OSWa8OccMG5XEUvHTZjmdwVT2\n"
-            + "czMss8nwgifU9D4hEVRu1MWpiyxUgegW94wuSh4PWIVOVd18PmzAYc73aYgonakb\n"
-            + "M+MDIqGVvAH8QtHo79sqZ9vrkQaQXB3Y8cq+WxDQZyl8KLXP2icmq1Rl6Q6+i9oS\n"
-            + "pFe88Wr0cGaTblkfDbbWcih3C6tKAfcFwLLg8u4jYfXjZg/E9eAJf0dIFcQSQoHd\n"
-            + "O8hVXaZwx/rYXA8UFwAuROo2nu6SIof1lrH92p+now95d5zUZ5BYnKVd3uXsln0j\n"
-            + "z585UPQKS2J8PUy9IirmahgTyEYFwO64kZ2B4hYOE2g+rYw=\n"
-            + "=5pIh\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =LtMR
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =5pIh
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -352,93 +364,97 @@
    */
   public static TestKey keyD() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C\n"
-            + "jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg\n"
-            + "DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO\n"
-            + "cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784\n"
-            + "9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx\n"
-            + "JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAG0HlRlc3R1c2VyIEQgPHRl\n"
-            + "c3RkQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQqZHi1Q/dNne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGq\n"
-            + "IDPhZFtPn0p2IAkqr5sAhvZAjd3u9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16\n"
-            + "aBK2ADq2YgPEmTToots1A0Tj+LaCFOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vY\n"
-            + "I/LtvThAk28D8yIfDnW49Mc4GGq+qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7\n"
-            + "Qw70Kqysaoy1KiPRAgwiPQfMCEx6pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhgu\n"
-            + "Q3Qe7xQlAtVObxskcTH2CWggl2dPqSMNieLK0g/ER8PIReGDCBXNSJ4qYbkBDQRM\n"
-            + "ep8JAQgAw/o1nhJPLGlIfEMzOGU0Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJq\n"
-            + "jSo7e9XC9jA2ih0+Gld0vWV7S0LZ84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWX\n"
-            + "QmY76hHIaF8rs6aJB7lRig735VRLxVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsT\n"
-            + "GRHgmydaxZbGXz+Z57jbQgm11CQEHX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNi\n"
-            + "xXHxryH2Jd34pA0cGHYVcTgVjXuZ9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN\n"
-            + "5Pxy5ocR7R2ZoN0pYD5+Cc7oGHjuCQARAQABiQEfBBgBAgAJBQJMep8JAhsMAAoJ\n"
-            + "EKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0KrausBHH161j\n"
-            + "lraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg9a2LWb4z\n"
-            + "rvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayboePRXdfr\n"
-            + "8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5QUig+c3oG\n"
-            + "a5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4C58w0Uvp\n"
-            + "HZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=\n"
-            + "=YDhQ\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C\n"
-            + "jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg\n"
-            + "DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO\n"
-            + "cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784\n"
-            + "9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx\n"
-            + "JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAEAB/0Yf+FiLHz/HYDbW9FF\n"
-            + "kmj7wXgFz7WRho6dsWQNxr5HmZZWxxFPMgJpONnc9GGOsApFAnLIrDraqX3AFFPO\n"
-            + "nxH36djfuPKcYqZ77Olm2vXGeWzqT0a2KN5zKQawH/1CxDUwe+Zx/60V8KAfXbSJ\n"
-            + "up+ymnAcbKa0VYYSYFI82/KTdthJ1jFMNtXkaLskpM8TrDBCgd38m8Dpb5GCrDVY\n"
-            + "faZgkHokTTrvaTcx7ebGOxlOcbfzOPMJyFiz6lHf4JGr5ZVQXymaAG18kRDFxXHm\n"
-            + "AskOJIxnMdcy2IzNximht2CIgRuGznyPoeh/j8KFONKIKf3N6dVfV12uIvGOVV+D\n"
-            + "/ZQZBAD2dennp3Z4IsOWkgHTG3bloOVcIY5n+WvliQY/5G3psKdKeaGZxt6MhMSj\n"
-            + "sJEiUgveYTt5PxvQc5jmFEyjEQJmDAHo3RbycdFVvICrKIhKFyIlcVFCOSwDvLAW\n"
-            + "aZhu/m47jGnnYZ+bDzZl4X8L7Zu8e3TStEiVhjYTRqJfdEdMVQQA+A0ehIhIa1mJ\n"
-            + "ytGKWQVxn9BwKTP583vf2qPzul7yDEsYdGfoA0QGUicVwV4NNK3vK3FQM9MBSevp\n"
-            + "JFpxh2bRS/tgd5tFDyRqekTcagMqTxnJoIpCPUvj5D+WXsS1Kwrcm7OpWoNHOcjD\n"
-            + "Hbhk/966QALO+T6BTVLx32/72jtQ10UD/RsqQfRDzlQUOd6ZYOlH5qCb1+f8f3qJ\n"
-            + "yUmudrmjj8unBK3QbBVrxZ1h9AyaI5evFmsMlLKdTp0y49CmrSQmgEnUYzvBDjse\n"
-            + "/jYanpRKnt69HeZFilHLIF+HBbQfSM66UVXVoJSNTJIsncVa0IcGoZTpCUVOng3/\n"
-            + "MLfW4sh9NX1yRIi0HlRlc3R1c2VyIEQgPHRlc3RkQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQqZHi1Q/d\n"
-            + "Nne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGqIDPhZFtPn0p2IAkqr5sAhvZAjd3u\n"
-            + "9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16aBK2ADq2YgPEmTToots1A0Tj+LaC\n"
-            + "FOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vYI/LtvThAk28D8yIfDnW49Mc4GGq+\n"
-            + "qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7Qw70Kqysaoy1KiPRAgwiPQfMCEx6\n"
-            + "pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhguQ3Qe7xQlAtVObxskcTH2CWggl2dP\n"
-            + "qSMNieLK0g/ER8PIReGDCBXNSJ4qYZ0DmARMep8JAQgAw/o1nhJPLGlIfEMzOGU0\n"
-            + "Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJqjSo7e9XC9jA2ih0+Gld0vWV7S0LZ\n"
-            + "84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWXQmY76hHIaF8rs6aJB7lRig735VRL\n"
-            + "xVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsTGRHgmydaxZbGXz+Z57jbQgm11CQE\n"
-            + "HX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNixXHxryH2Jd34pA0cGHYVcTgVjXuZ\n"
-            + "9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN5Pxy5ocR7R2ZoN0pYD5+Cc7oGHju\n"
-            + "CQARAQABAAf/QiN/k9y+/pB7h4BQWXCCNIIYb6zqGuzUSdYZWuYHwiEL1f05SFmp\n"
-            + "VjDE5+ZAU+8U0Gv+BAeRbWdlfQOyI/ioQJL1DggeXqanUF4uCbjGDBPLhtCZsmmM\n"
-            + "QVLdrOl+v+SHe33e7E7AQSyQMaUSkUEtHycYIasZPQRfw9H/L3u9OEWXkMUbPso5\n"
-            + "L0A0StkcsM1isYfC8ApnF4zSTWHO9uqnc+qE4qChCqsGvaSIyLKEpVe4F0vEkbrq\n"
-            + "3usVp3cxJd9apN+JjMoC9dHJcQahgfJZ1jzgJ3rueRxrGZV+keo8VmyrDGFCerX9\n"
-            + "6Ke3RPMHN/evCHyPMtHC82QKYuy4ZTvldwQAyzbNKIIpNjyHRc/hXLMBUtnW0VYS\n"
-            + "dELA1VBMmT/d6Xx6pI9gg9HCjDx+DuQRych7ShxrYLL1pNQD8jwEJhZIeUpSgIFD\n"
-            + "BXdwkiGbmdrU5N0tBhxp8kRcqcGbL68zC9S0X2hNju6Dxu9hbG8ZAdYaCdAavVy0\n"
-            + "O6E66+T0cLRBinsEAPbiL/0rpV15DdITwD3hvzhYDyURE+yxQZe9ngS1uoui3mGn\n"
-            + "bLc/L/nbHf2Z91ViSsUaqJjpb2/eDsJtGJ9pFlFLTndujkA62CktJytD9DIYLlYD\n"
-            + "huXlsKvZkNZEZNDKLC5Tg8YR/28Opz0/ZFzfVuJAQqg7+iWkxklG3SvN71RLA/9x\n"
-            + "wun1AEw6tLJ2R2j8+yXIt8UaWExqAviT/JgZELVXdCTqcYuOmktsM2z+2D+OyUtP\n"
-            + "7+Yyz7MGQKMAU+V/1uOK4YqwUJrcGy501o9Of+xm+5DASsK1oM5e9sBdmNewdLHL\n"
-            + "ZJEllURrEC6zCE/4zzs7qUfakH4l4ZJgjRL6va+ED0HfiQEfBBgBAgAJBQJMep8J\n"
-            + "AhsMAAoJEKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0Krau\n"
-            + "sBHH161jlraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg\n"
-            + "9a2LWb4zrvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayb\n"
-            + "oePRXdfr8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5Q\n"
-            + "Uig+c3oGa5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4\n"
-            + "C58w0UvpHZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=\n"
-            + "=e1xT\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C
+        jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg
+        DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO
+        cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784
+        9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx
+        JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAG0HlRlc3R1c2VyIEQgPHRl
+        c3RkQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoL
+        BBYCAwECHgECF4AACgkQqZHi1Q/dNne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGq
+        IDPhZFtPn0p2IAkqr5sAhvZAjd3u9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16
+        aBK2ADq2YgPEmTToots1A0Tj+LaCFOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vY
+        I/LtvThAk28D8yIfDnW49Mc4GGq+qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7
+        Qw70Kqysaoy1KiPRAgwiPQfMCEx6pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhgu
+        Q3Qe7xQlAtVObxskcTH2CWggl2dPqSMNieLK0g/ER8PIReGDCBXNSJ4qYbkBDQRM
+        ep8JAQgAw/o1nhJPLGlIfEMzOGU0Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJq
+        jSo7e9XC9jA2ih0+Gld0vWV7S0LZ84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWX
+        QmY76hHIaF8rs6aJB7lRig735VRLxVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsT
+        GRHgmydaxZbGXz+Z57jbQgm11CQEHX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNi
+        xXHxryH2Jd34pA0cGHYVcTgVjXuZ9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN
+        5Pxy5ocR7R2ZoN0pYD5+Cc7oGHjuCQARAQABiQEfBBgBAgAJBQJMep8JAhsMAAoJ
+        EKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0KrausBHH161j
+        lraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg9a2LWb4z
+        rvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayboePRXdfr
+        8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5QUig+c3oG
+        a5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4C58w0Uvp
+        HZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=
+        =YDhQ
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        lQOYBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C
+        jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg
+        DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO
+        cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784
+        9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx
+        JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAEAB/0Yf+FiLHz/HYDbW9FF
+        kmj7wXgFz7WRho6dsWQNxr5HmZZWxxFPMgJpONnc9GGOsApFAnLIrDraqX3AFFPO
+        nxH36djfuPKcYqZ77Olm2vXGeWzqT0a2KN5zKQawH/1CxDUwe+Zx/60V8KAfXbSJ
+        up+ymnAcbKa0VYYSYFI82/KTdthJ1jFMNtXkaLskpM8TrDBCgd38m8Dpb5GCrDVY
+        faZgkHokTTrvaTcx7ebGOxlOcbfzOPMJyFiz6lHf4JGr5ZVQXymaAG18kRDFxXHm
+        AskOJIxnMdcy2IzNximht2CIgRuGznyPoeh/j8KFONKIKf3N6dVfV12uIvGOVV+D
+        /ZQZBAD2dennp3Z4IsOWkgHTG3bloOVcIY5n+WvliQY/5G3psKdKeaGZxt6MhMSj
+        sJEiUgveYTt5PxvQc5jmFEyjEQJmDAHo3RbycdFVvICrKIhKFyIlcVFCOSwDvLAW
+        aZhu/m47jGnnYZ+bDzZl4X8L7Zu8e3TStEiVhjYTRqJfdEdMVQQA+A0ehIhIa1mJ
+        ytGKWQVxn9BwKTP583vf2qPzul7yDEsYdGfoA0QGUicVwV4NNK3vK3FQM9MBSevp
+        JFpxh2bRS/tgd5tFDyRqekTcagMqTxnJoIpCPUvj5D+WXsS1Kwrcm7OpWoNHOcjD
+        Hbhk/966QALO+T6BTVLx32/72jtQ10UD/RsqQfRDzlQUOd6ZYOlH5qCb1+f8f3qJ
+        yUmudrmjj8unBK3QbBVrxZ1h9AyaI5evFmsMlLKdTp0y49CmrSQmgEnUYzvBDjse
+        /jYanpRKnt69HeZFilHLIF+HBbQfSM66UVXVoJSNTJIsncVa0IcGoZTpCUVOng3/
+        MLfW4sh9NX1yRIi0HlRlc3R1c2VyIEQgPHRlc3RkQGV4YW1wbGUuY29tPokBOAQT
+        AQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQqZHi1Q/d
+        Nne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGqIDPhZFtPn0p2IAkqr5sAhvZAjd3u
+        9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16aBK2ADq2YgPEmTToots1A0Tj+LaC
+        FOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vYI/LtvThAk28D8yIfDnW49Mc4GGq+
+        qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7Qw70Kqysaoy1KiPRAgwiPQfMCEx6
+        pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhguQ3Qe7xQlAtVObxskcTH2CWggl2dP
+        qSMNieLK0g/ER8PIReGDCBXNSJ4qYZ0DmARMep8JAQgAw/o1nhJPLGlIfEMzOGU0
+        Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJqjSo7e9XC9jA2ih0+Gld0vWV7S0LZ
+        84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWXQmY76hHIaF8rs6aJB7lRig735VRL
+        xVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsTGRHgmydaxZbGXz+Z57jbQgm11CQE
+        HX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNixXHxryH2Jd34pA0cGHYVcTgVjXuZ
+        9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN5Pxy5ocR7R2ZoN0pYD5+Cc7oGHju
+        CQARAQABAAf/QiN/k9y+/pB7h4BQWXCCNIIYb6zqGuzUSdYZWuYHwiEL1f05SFmp
+        VjDE5+ZAU+8U0Gv+BAeRbWdlfQOyI/ioQJL1DggeXqanUF4uCbjGDBPLhtCZsmmM
+        QVLdrOl+v+SHe33e7E7AQSyQMaUSkUEtHycYIasZPQRfw9H/L3u9OEWXkMUbPso5
+        L0A0StkcsM1isYfC8ApnF4zSTWHO9uqnc+qE4qChCqsGvaSIyLKEpVe4F0vEkbrq
+        3usVp3cxJd9apN+JjMoC9dHJcQahgfJZ1jzgJ3rueRxrGZV+keo8VmyrDGFCerX9
+        6Ke3RPMHN/evCHyPMtHC82QKYuy4ZTvldwQAyzbNKIIpNjyHRc/hXLMBUtnW0VYS
+        dELA1VBMmT/d6Xx6pI9gg9HCjDx+DuQRych7ShxrYLL1pNQD8jwEJhZIeUpSgIFD
+        BXdwkiGbmdrU5N0tBhxp8kRcqcGbL68zC9S0X2hNju6Dxu9hbG8ZAdYaCdAavVy0
+        O6E66+T0cLRBinsEAPbiL/0rpV15DdITwD3hvzhYDyURE+yxQZe9ngS1uoui3mGn
+        bLc/L/nbHf2Z91ViSsUaqJjpb2/eDsJtGJ9pFlFLTndujkA62CktJytD9DIYLlYD
+        huXlsKvZkNZEZNDKLC5Tg8YR/28Opz0/ZFzfVuJAQqg7+iWkxklG3SvN71RLA/9x
+        wun1AEw6tLJ2R2j8+yXIt8UaWExqAviT/JgZELVXdCTqcYuOmktsM2z+2D+OyUtP
+        7+Yyz7MGQKMAU+V/1uOK4YqwUJrcGy501o9Of+xm+5DASsK1oM5e9sBdmNewdLHL
+        ZJEllURrEC6zCE/4zzs7qUfakH4l4ZJgjRL6va+ED0HfiQEfBBgBAgAJBQJMep8J
+        AhsMAAoJEKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0Krau
+        sBHH161jlraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg
+        9a2LWb4zrvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayb
+        oePRXdfr8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5Q
+        Uig+c3oGa5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4
+        C58w0UvpHZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=
+        =e1xT
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -447,93 +463,97 @@
    */
   public static TestKey keyE() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J\n"
-            + "yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda\n"
-            + "zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I\n"
-            + "K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR\n"
-            + "4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo\n"
-            + "+UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAG0HlRlc3R1c2VyIEUgPHRl\n"
-            + "c3RlQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIG\n"
-            + "FQgCCQoLBBYCAwECHgECF4AACgkQOwm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0q\n"
-            + "zoLZrHwCFcaeO3kz53y5Lz3+plMuqVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6\n"
-            + "f0MpguTGclvFroevUct0xiyox5r1DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9\n"
-            + "EsHsF+/3RBbsXbQgDpW38g0GzIJI4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGj\n"
-            + "yPhatE7Zu2ABNcerIDstupWww2Psec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJS\n"
-            + "kgHScOzTElIQqOA1+w6uiHy2oAn+qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVy\n"
-            + "KLkBDQRMep8aAQgAn5r6toYnEzwDeig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBW\n"
-            + "HUlqV8sglQ9aINpGtBf37v13RhtU3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5\n"
-            + "FdzTm4C4WaoE7QiTRbiekwh7O54mz4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1q\n"
-            + "UEsKNnITW+mWHY3+ccK1hgqPwOPqO3/8QtaipekKOYAtOb+57c1jtDFBZnYIkant\n"
-            + "oKs+kRw0DykXFTyFOMYqaleBMcVG+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69h\n"
-            + "RH0Ebn50ebpoqKOXhN4/bu/wq596y0o4xDB0GQARAQABiQElBBgBAgAPBQJMep8a\n"
-            + "AhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2LBqeXN/b\n"
-            + "CLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2dM9S1AzE\n"
-            + "H+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPNgag6mPnD\n"
-            + "zd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBKDUCdrl79\n"
-            + "0u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm1pPcLQHR\n"
-            + "6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=\n"
-            + "=uA5x\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J\n"
-            + "yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda\n"
-            + "zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I\n"
-            + "K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR\n"
-            + "4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo\n"
-            + "+UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAEAB/4xKKzYqDVyM/2NN5Mi\n"
-            + "fF3EqegruzRESzlgrqLij5LiU1sGLOLbjunC/pPWMu6t+rTYV0pT3hmb5D0eAcH0\n"
-            + "EcANiuAR0wg1P9yNk36Z54mLWoTzzKMb3dunCSvb+BU8AREKZ4v5dLEGz2lK7DPo\n"
-            + "zbhWaffMiClBpC0VbjfFBo91LrVUVnhRglBYKdPLQm/Lhw5cNCYOw194ZturO+cC\n"
-            + "iQZhGSy52HMoMs4Wr470CeFZvvWaiDCirVLcj4UhMsVANFKsahMARm9c+QrGrkRP\n"
-            + "+654f8M9ptapcQYpGOMmaeZVnpocONXOTkiJd7Hhr4PRUY+QS8C8F0LbmL2ERQbL\n"
-            + "F65RBADkIelztY/8Xy2S0jsW7+xF2ziz9riOR87G6b0wrXDdFz4GHPzLvwsdXOeN\n"
-            + "cODic14d9bf5jtXr9hgbAzx55ANDjOl3jK5qil8Z9qwsrNK9Mz0wT1acQXBwf/5D\n"
-            + "hI/whBK1FsH7Y+wdX64XA3EXmclxB8GZf1JsGXF3jNH30vyS7QQA/ydoMMw8ja9L\n"
-            + "j6MxHtVHcE4A4j6tFljLDuf8icOwwNUfb7SsHTDjUI2+30ZJOv+qISrthsASCSj3\n"
-            + "AN87CGdVR62Xe923DNdW8/moKKDILNaESyOi27qhI5qWrVRgNB5QwbQcSoClUxbj\n"
-            + "V7YZSfrZkiI+GE1gh1QPMOVyCUmqu90D+wc0x0wUj8emX/4xbbujOa5RAvNcNvnD\n"
-            + "mOB2CfPWD10TEeOOlHBhuoy2/GdIl76W0szJaxnzcV82VArllSciCBzpSfkExDZ6\n"
-            + "08hA8GpOsuOmAAPwXWZsb8YZbJeM0ULMgUCGHgvUj1/pGsCVA6c7sPAdkCfAFlmO\n"
-            + "smC9bvpS2VHZPuG0HlRlc3R1c2VyIEUgPHRlc3RlQGV4YW1wbGUuY29tPokBPgQT\n"
-            + "AQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ\n"
-            + "Owm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0qzoLZrHwCFcaeO3kz53y5Lz3+plMu\n"
-            + "qVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6f0MpguTGclvFroevUct0xiyox5r1\n"
-            + "DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9EsHsF+/3RBbsXbQgDpW38g0GzIJI\n"
-            + "4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGjyPhatE7Zu2ABNcerIDstupWww2Ps\n"
-            + "ec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJSkgHScOzTElIQqOA1+w6uiHy2oAn+\n"
-            + "qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVyKJ0DmARMep8aAQgAn5r6toYnEzwD\n"
-            + "eig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBWHUlqV8sglQ9aINpGtBf37v13RhtU\n"
-            + "3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5FdzTm4C4WaoE7QiTRbiekwh7O54m\n"
-            + "z4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1qUEsKNnITW+mWHY3+ccK1hgqPwOPq\n"
-            + "O3/8QtaipekKOYAtOb+57c1jtDFBZnYIkantoKs+kRw0DykXFTyFOMYqaleBMcVG\n"
-            + "+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69hRH0Ebn50ebpoqKOXhN4/bu/wq596\n"
-            + "y0o4xDB0GQARAQABAAf7Bk9bQCIXo2QJAyhaFd5qh10qhu7CyRnvG/8zKMW98mWd\n"
-            + "KxF+9hNz99qZBCuiNZBLoU0dST6OG6By/3nrDxXxAgZS3cgOj/nl1NJTRWDGHPUu\n"
-            + "LywFgj7Dwu8Y2rqlDTX8lJIS+t8n+BhtkmDHoesGmFtErh8nT/CxQuHLM60qSMgv\n"
-            + "6mSmtOkM+2KfiA5z2o1fDWXjDieW+hdgDPxkaB835wfuDn/Dsn1ch1XHON0xSyTo\n"
-            + "+c35nFXoK1pAXaoalAxZNxcXCAM3NhU37Ih4GejM0K7sSgK72HmgxtNYF77DrTIM\n"
-            + "m5+3960ri1JUuEaJ7ZcqbpKxy/GDldNCYBTx07QMzQQAyYQ+ujT9Pj8zfp1jMLRs\n"
-            + "Xn9GsvYawjo+AIZuHeUmmIXfEoyNmsEUoGHnz9ROLnJzanW5XEStiTys8tHJPIkz\n"
-            + "zL0Ce0oUF93ln0z/jQBIKaSzYB7PMmYCd7ueF94aKqAOrQ/QBb+6JsVjGAtLUoTv\n"
-            + "ey09hGYMogiBV1r0MB2Rsa8EAMrB5VKVQF6+q0XuP6ljFQRaumi4lH7PoQ65E7UD\n"
-            + "6YpyQpLBOE7dV+fHizdUuwsD/wyAOu0EskV1ZLXvXzyk10r3PRoFdpHOvijwZBGt\n"
-            + "jiOiVvK1vkQKDMBczOe74+DaknKn6HzgCsXmLgfk+P8BtLOJnCYsbS9IbnImy2vi\n"
-            + "aJC3A/9wOOK+po8C7JPHVIEfxbe7nwHOoi/h7T4uPrlq/gcQRquqGhQ16nDGYZvX\n"
-            + "ny9aPQ3NcvDR69RM2AaXav03bHVxfhVEyGjP5jLZz7956e4LlnKrsuEhDLfiv30i\n"
-            + "qCC7zNHNA99s5u25vt8AuPVVHfSQ++jifabfv5lU4FHqmK8/4EAoiQElBBgBAgAP\n"
-            + "BQJMep8aAhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2\n"
-            + "LBqeXN/bCLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2\n"
-            + "dM9S1AzEH+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPN\n"
-            + "gag6mPnDzd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBK\n"
-            + "DUCdrl790u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm\n"
-            + "1pPcLQHR6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=\n"
-            + "=HTKj\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J
+        yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda
+        zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I
+        K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR
+        4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo
+        +UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAG0HlRlc3R1c2VyIEUgPHRl
+        c3RlQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIG
+        FQgCCQoLBBYCAwECHgECF4AACgkQOwm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0q
+        zoLZrHwCFcaeO3kz53y5Lz3+plMuqVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6
+        f0MpguTGclvFroevUct0xiyox5r1DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9
+        EsHsF+/3RBbsXbQgDpW38g0GzIJI4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGj
+        yPhatE7Zu2ABNcerIDstupWww2Psec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJS
+        kgHScOzTElIQqOA1+w6uiHy2oAn+qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVy
+        KLkBDQRMep8aAQgAn5r6toYnEzwDeig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBW
+        HUlqV8sglQ9aINpGtBf37v13RhtU3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5
+        FdzTm4C4WaoE7QiTRbiekwh7O54mz4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1q
+        UEsKNnITW+mWHY3+ccK1hgqPwOPqO3/8QtaipekKOYAtOb+57c1jtDFBZnYIkant
+        oKs+kRw0DykXFTyFOMYqaleBMcVG+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69h
+        RH0Ebn50ebpoqKOXhN4/bu/wq596y0o4xDB0GQARAQABiQElBBgBAgAPBQJMep8a
+        AhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2LBqeXN/b
+        CLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2dM9S1AzE
+        H+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPNgag6mPnD
+        zd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBKDUCdrl79
+        0u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm1pPcLQHR
+        6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=
+        =uA5x
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =HTKj
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -542,99 +562,103 @@
    */
   public static TestKey keyF() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx+aQkBCADycHmQ8wC4GzaDIvw4uv6/HiTWOUmuLcT06PpsvNBdR2sQ6Vyy\n"
-            + "w1SAnaPmskdgxE7TXpDrwIWPmIkg8KzSfPAap6qZy5zyE1ABQa9yD9v6wsew+lM5\n"
-            + "3UdBO6HQodpWSJMbeR48mUQ96z72B7Lb2GvhFLxvcn5od9jQhbQXfb2k67l33hgR\n"
-            + "D427lxXa+qnmL9pMRGhRw6QATFX+icHxsPfpKnzuk0aY3feJm4+jr4RgHP4djH3i\n"
-            + "NZbv3ibZ24Dj3CK07PwbhqUhZwMqueWbo3ChYjLkRGT/UosNTN0EbHjqBMl4N9OT\n"
-            + "Pl2CM6kuzuaLz3ZeAf48B29GX4rAXfuJTKBbABEBAAG0HlRlc3R1c2VyIEYgPHRl\n"
-            + "c3RmQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pCQIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQSp+scDH6SMRfqggAh//U/l4JuwFgWx14mo0SB9YWU81L\n"
-            + "EgUYUd2MUzvX4p/HIFQa0c7stj68Za40+O0tG/J0RCMNb7piM9JFii+MQZzOVuza\n"
-            + "4bbO59D9qboc7Anvx9hGlfIdinT+n5rwX9kZvD2D7GMskm8ZgovkvNwNKcW+5W/4\n"
-            + "ciWqCJKE/Fp9XsooJgN94pJfgDQ2WBL5KDx1aGt4wZXhH2Atl6a6oVZJIH4SaizD\n"
-            + "jau7F4vc7hBfbcDhxFcrVX1QMpzpl352cIx6KVw4FRWvQ8VKkga4JiQwosfvCT2Z\n"
-            + "pdMwy3cARynv8BWLc4Uexf88QIeClP9ZhoVeMqvHMfUb3d6Q5362VdZqI4kBIAQQ\n"
-            + "AQIACgUCTH5xcgMFCngACgkQiptSk+LTK6UqsAgAlsEmzC3Xxv4o5ui95AFbWZGi\n"
-            + "es5rI9WoW2P+6OqVUy1E8+5HdlJ8wUbU1H7JAdFTjY9rH3vKXCXsTetF4z0cupER\n"
-            + "Rkx06M9/jl5OSw8i9bPNNJFobHwiiNO00ctC1tT5oUVXVsfPQHlEbMofv8jehfgC\n"
-            + "gMqH/ve/aafKFfYCZkNHugRgLzxeDpXp3IdyXoSAFGiULnGvMDN7n61QOvEYOw2Z\n"
-            + "i63ql+bL2oj4G+/bNOkdYkuIBN4F/P45P7xy80MSOvkMH7IG/aFTKMNQGWSykKwI\n"
-            + "FRkC+y+F5Oqf/WD30GvbSA7q013sb6nHYvsaHS/48cgIJ5TSVd0LTlrF9uv43bkB\n"
-            + "DQRMfmkJAQgAzc1uAF4x16Cx4GtHI0Hvm+v7bUEUtBw2XzyOKu883XC5JmGcY18y\n"
-            + "YItRpchAtmacDpu0/2925/mWF7aS9RMgSYI/1D9LaTeimISM3iGFY35kt78NGZwJ\n"
-            + "DeCPJPI1sbOU0njfrCPTbOQuRDJ6evaBNX9HYArSEp0ygruJdOUYgnepCt4A7W95\n"
-            + "EKp9KPo7XV1K8y86vrKbgpJ+NnEi7dzMqVxnhO4wAWqb6HYcKLrEc2gVnLtzHkBl\n"
-            + "Y/6dOP15jgQKql1/yQIXae/WGT24n/VeaKqrbSmDNkhW5eW5o1Bkgy/M98oNHXd0\n"
-            + "nVrT8Lyf6un5TwMy+vk0l5AjMMtIZKS0GQARAQABiQEfBBgBAgAJBQJMfmkJAhsM\n"
-            + "AAoJEEqfrHAx+kjEvDAH/iO6BHQfFa+kqjfYD3NE+FNosXv3jiXOU7SCD2MG3AwD\n"
-            + "YqM+v1n4UvvMLLdEbtboht1Btys1vuyNM3RAmR45oh9Dfuc4SKtVzSCkKs85jNvH\n"
-            + "7Ik8gxZ9ARzJbawNzTLFyLwDdcdX42Umuvh49Pn7Nc7FDYcZLffEcTh9sZ7KyxLY\n"
-            + "qcjtnblx5oOQnYnpBbM61GvgNXC8Z+g9fg0oHRouKXKE/HDKbsN0siEf9XJFJTKd\n"
-            + "Eg1NgoyKWdaV4+pU/fTzZUvvDqOSRx8he5w64dvW9o7WdARq/3vPvHgy0O8fMTSI\n"
-            + "tmcHxCU8l0jptJz181N36Uhmjyc9oC4dn9ceSn6VDbg=\n"
-            + "=WDx2\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx+aQkBCADycHmQ8wC4GzaDIvw4uv6/HiTWOUmuLcT06PpsvNBdR2sQ6Vyy\n"
-            + "w1SAnaPmskdgxE7TXpDrwIWPmIkg8KzSfPAap6qZy5zyE1ABQa9yD9v6wsew+lM5\n"
-            + "3UdBO6HQodpWSJMbeR48mUQ96z72B7Lb2GvhFLxvcn5od9jQhbQXfb2k67l33hgR\n"
-            + "D427lxXa+qnmL9pMRGhRw6QATFX+icHxsPfpKnzuk0aY3feJm4+jr4RgHP4djH3i\n"
-            + "NZbv3ibZ24Dj3CK07PwbhqUhZwMqueWbo3ChYjLkRGT/UosNTN0EbHjqBMl4N9OT\n"
-            + "Pl2CM6kuzuaLz3ZeAf48B29GX4rAXfuJTKBbABEBAAEAB/4vTP+C5s5snS6ZDlHc\n"
-            + "datvOV/hhgLYn2huiigV4A7dLCp4/bbOz+pkP51zTLQ9bn+coLYwsPq+Bfo3OY3W\n"
-            + "cXbdFHpmEEJaPqdc32ZuICcAuVEBuA1V3FTjJtHO5U02iWleMlbSZurYE9ZQZTch\n"
-            + "yotdulB7hACivENKh9OXw7ok+1GZVvBGA8tpIwzLZo0Pkb2lDQHaL0GXAjlMNzwg\n"
-            + "cCPFtzjNu6K4g58nuYrjGiE+yWPMJgfo4fTGXcapqXgvh1tKIVxwr2YQSyEOqfMH\n"
-            + "8EwgBj5NPwv0UXAivQUkTaguUJXrlJLtS3mp45nCEAlGT4PNoMyPdvPEf62gND7C\n"
-            + "y9K1BAD493ADPAx9pWCSQI9wp4ARUelTzwHgZ6fRVIzmwO6MuZN1PrtiOLCwY5Jw\n"
-            + "r+97VvMmem7Ya3khP4vz0IiN7p1oCR5nJazk2eRaQNuim0aB0lqrTsli8OXtBlgQ\n"
-            + "5WtLcRi5798Jw8coczc5OftZKhu1SbQZ1VdDdmTbMTAsSRtMjQQA+UnU6FYJZBjE\n"
-            + "NHNheV6+k45HXHubcCm4Ka3kJK88zbZzyt+nrBLEtElosxDCqT8WbiAH7qmpnd/r\n"
-            + "ly7ryIX08etuWVYnx0Xa02cKQ6TzNcbxijeGQYGHIE0RK29nRo8zRWVmbCydqJz1\n"
-            + "5cHgcvoTu7DWWjM5QEZlLPQytJeAyocEAM6AiWDXYVZVnCB9w0wwK/9cX0v3tfYv\n"
-            + "QrJZCT3/YKxJWnMZ+LgHYO0w1B0YwGEeVTnmXODDy5mRh9lxV1aZnwKCwMR1tXTx\n"
-            + "G1potBR0GJxI2xpMb/MJPxeJCAZPu8NncRpl/8v0stiGnkpYCNR/k3JV5jEXq0u6\n"
-            + "4pDSzRGehOHnOqu0HlRlc3R1c2VyIEYgPHRlc3RmQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTH5pCQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQSp+scDH6\n"
-            + "SMRfqggAh//U/l4JuwFgWx14mo0SB9YWU81LEgUYUd2MUzvX4p/HIFQa0c7stj68\n"
-            + "Za40+O0tG/J0RCMNb7piM9JFii+MQZzOVuza4bbO59D9qboc7Anvx9hGlfIdinT+\n"
-            + "n5rwX9kZvD2D7GMskm8ZgovkvNwNKcW+5W/4ciWqCJKE/Fp9XsooJgN94pJfgDQ2\n"
-            + "WBL5KDx1aGt4wZXhH2Atl6a6oVZJIH4SaizDjau7F4vc7hBfbcDhxFcrVX1QMpzp\n"
-            + "l352cIx6KVw4FRWvQ8VKkga4JiQwosfvCT2ZpdMwy3cARynv8BWLc4Uexf88QIeC\n"
-            + "lP9ZhoVeMqvHMfUb3d6Q5362VdZqI50DmARMfmkJAQgAzc1uAF4x16Cx4GtHI0Hv\n"
-            + "m+v7bUEUtBw2XzyOKu883XC5JmGcY18yYItRpchAtmacDpu0/2925/mWF7aS9RMg\n"
-            + "SYI/1D9LaTeimISM3iGFY35kt78NGZwJDeCPJPI1sbOU0njfrCPTbOQuRDJ6evaB\n"
-            + "NX9HYArSEp0ygruJdOUYgnepCt4A7W95EKp9KPo7XV1K8y86vrKbgpJ+NnEi7dzM\n"
-            + "qVxnhO4wAWqb6HYcKLrEc2gVnLtzHkBlY/6dOP15jgQKql1/yQIXae/WGT24n/Ve\n"
-            + "aKqrbSmDNkhW5eW5o1Bkgy/M98oNHXd0nVrT8Lyf6un5TwMy+vk0l5AjMMtIZKS0\n"
-            + "GQARAQABAAf/T22JFmhESUnSTOBqeK+Sd/WIOJ7lDCxVScVXwzdJINfIBYmnr2yG\n"
-            + "x18NuHOEkkEg2rx6ixksZZRcurMynZZvoB8+Xj69bpLT1JRXv8VlM0SNP6NjPW6M\n"
-            + "ygfQhzxZv8ck2WRgQxIin8SjHJv0zG9F5+1DEUyrzhZQb8dMYkqm/nbZ1FDnMu4F\n"
-            + "1qUZxKx0hU70tAXfywtpH9NQs8jwenUjiXA00k6A48BF7gartYtcGnEG9mk+Z+lh\n"
-            + "/uD+z5j3/ym9XqOJPpFIWhMYTLueSD5yrCT34VdIc1xBOjjtxBsCCbgSFZaewCpB\n"
-            + "5usRr2I4+CK3vbAMny5Hk+/RYZdFQkCA5wQA2JusdhwqPjfzxtcxz13Vu1ZzKR41\n"
-            + "kkno/boGh5afBlf7kL/5FXDhGVVvHMvXtQntU1kHgOcE8b2Jfy38gNGkd3TAh4Oj\n"
-            + "fLavcYyn+9tEkjRVdOeU0P9fszDA1cW5Gjuv6GkbCUSQrv68TKp/mWiTlYm+FT3a\n"
-            + "RSIz2gEyOZNkTzsEAPM6sU/VOwpJ2ppOa5+290sptjSbRNYjKlQ66nHZnbafzLz5\n"
-            + "tKpRc0BzG/N2lXwlVl5+3oXSSSbWhJscA8EFwSnAx8Id10zW5NAEfxNuqxxEXlJg\n"
-            + "kOhqwJ1JMz32xlZFRZYxSdXSycYrX/AhV7I7RQxgC48X9udMb8LIXYq0lzy7A/9p\n"
-            + "Skd2Me9JotuTN3OaR42hXozLx+yERBBEWuI3WXovWRD8b8gCfWL3P40d2UVnjFmP\n"
-            + "TZ8p9aHAd2srWgaPSZaSsHtIyI6dQGScMEOKEaCJxYvF/wuvx/MABDatcaJhMaAc\n"
-            + "W/0w+gb8Lr2hbuRhBSP754V3Amma6LxsmLRAwB6ioT7NiQEfBBgBAgAJBQJMfmkJ\n"
-            + "AhsMAAoJEEqfrHAx+kjEvDAH/iO6BHQfFa+kqjfYD3NE+FNosXv3jiXOU7SCD2MG\n"
-            + "3AwDYqM+v1n4UvvMLLdEbtboht1Btys1vuyNM3RAmR45oh9Dfuc4SKtVzSCkKs85\n"
-            + "jNvH7Ik8gxZ9ARzJbawNzTLFyLwDdcdX42Umuvh49Pn7Nc7FDYcZLffEcTh9sZ7K\n"
-            + "yxLYqcjtnblx5oOQnYnpBbM61GvgNXC8Z+g9fg0oHRouKXKE/HDKbsN0siEf9XJF\n"
-            + "JTKdEg1NgoyKWdaV4+pU/fTzZUvvDqOSRx8he5w64dvW9o7WdARq/3vPvHgy0O8f\n"
-            + "MTSItmcHxCU8l0jptJz181N36Uhmjyc9oC4dn9ceSn6VDbg=\n"
-            + "=ZLpl\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx+aQkBCADycHmQ8wC4GzaDIvw4uv6/HiTWOUmuLcT06PpsvNBdR2sQ6Vyy
+        w1SAnaPmskdgxE7TXpDrwIWPmIkg8KzSfPAap6qZy5zyE1ABQa9yD9v6wsew+lM5
+        3UdBO6HQodpWSJMbeR48mUQ96z72B7Lb2GvhFLxvcn5od9jQhbQXfb2k67l33hgR
+        D427lxXa+qnmL9pMRGhRw6QATFX+icHxsPfpKnzuk0aY3feJm4+jr4RgHP4djH3i
+        NZbv3ibZ24Dj3CK07PwbhqUhZwMqueWbo3ChYjLkRGT/UosNTN0EbHjqBMl4N9OT
+        Pl2CM6kuzuaLz3ZeAf48B29GX4rAXfuJTKBbABEBAAG0HlRlc3R1c2VyIEYgPHRl
+        c3RmQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pCQIbAwYLCQgHAwIGFQgCCQoL
+        BBYCAwECHgECF4AACgkQSp+scDH6SMRfqggAh//U/l4JuwFgWx14mo0SB9YWU81L
+        EgUYUd2MUzvX4p/HIFQa0c7stj68Za40+O0tG/J0RCMNb7piM9JFii+MQZzOVuza
+        4bbO59D9qboc7Anvx9hGlfIdinT+n5rwX9kZvD2D7GMskm8ZgovkvNwNKcW+5W/4
+        ciWqCJKE/Fp9XsooJgN94pJfgDQ2WBL5KDx1aGt4wZXhH2Atl6a6oVZJIH4SaizD
+        jau7F4vc7hBfbcDhxFcrVX1QMpzpl352cIx6KVw4FRWvQ8VKkga4JiQwosfvCT2Z
+        pdMwy3cARynv8BWLc4Uexf88QIeClP9ZhoVeMqvHMfUb3d6Q5362VdZqI4kBIAQQ
+        AQIACgUCTH5xcgMFCngACgkQiptSk+LTK6UqsAgAlsEmzC3Xxv4o5ui95AFbWZGi
+        es5rI9WoW2P+6OqVUy1E8+5HdlJ8wUbU1H7JAdFTjY9rH3vKXCXsTetF4z0cupER
+        Rkx06M9/jl5OSw8i9bPNNJFobHwiiNO00ctC1tT5oUVXVsfPQHlEbMofv8jehfgC
+        gMqH/ve/aafKFfYCZkNHugRgLzxeDpXp3IdyXoSAFGiULnGvMDN7n61QOvEYOw2Z
+        i63ql+bL2oj4G+/bNOkdYkuIBN4F/P45P7xy80MSOvkMH7IG/aFTKMNQGWSykKwI
+        FRkC+y+F5Oqf/WD30GvbSA7q013sb6nHYvsaHS/48cgIJ5TSVd0LTlrF9uv43bkB
+        DQRMfmkJAQgAzc1uAF4x16Cx4GtHI0Hvm+v7bUEUtBw2XzyOKu883XC5JmGcY18y
+        YItRpchAtmacDpu0/2925/mWF7aS9RMgSYI/1D9LaTeimISM3iGFY35kt78NGZwJ
+        DeCPJPI1sbOU0njfrCPTbOQuRDJ6evaBNX9HYArSEp0ygruJdOUYgnepCt4A7W95
+        EKp9KPo7XV1K8y86vrKbgpJ+NnEi7dzMqVxnhO4wAWqb6HYcKLrEc2gVnLtzHkBl
+        Y/6dOP15jgQKql1/yQIXae/WGT24n/VeaKqrbSmDNkhW5eW5o1Bkgy/M98oNHXd0
+        nVrT8Lyf6un5TwMy+vk0l5AjMMtIZKS0GQARAQABiQEfBBgBAgAJBQJMfmkJAhsM
+        AAoJEEqfrHAx+kjEvDAH/iO6BHQfFa+kqjfYD3NE+FNosXv3jiXOU7SCD2MG3AwD
+        YqM+v1n4UvvMLLdEbtboht1Btys1vuyNM3RAmR45oh9Dfuc4SKtVzSCkKs85jNvH
+        7Ik8gxZ9ARzJbawNzTLFyLwDdcdX42Umuvh49Pn7Nc7FDYcZLffEcTh9sZ7KyxLY
+        qcjtnblx5oOQnYnpBbM61GvgNXC8Z+g9fg0oHRouKXKE/HDKbsN0siEf9XJFJTKd
+        Eg1NgoyKWdaV4+pU/fTzZUvvDqOSRx8he5w64dvW9o7WdARq/3vPvHgy0O8fMTSI
+        tmcHxCU8l0jptJz181N36Uhmjyc9oC4dn9ceSn6VDbg=
+        =WDx2
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =ZLpl
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -643,99 +667,103 @@
    */
   public static TestKey keyG() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx+aRYBCAC77YjBScTjRFHtZvk0yyAy8KAXopbCdMBQs7S7iidFMMxhs0Uu\n"
-            + "D7GeleyVusLFJfEM0Ul0b0pLgfJx9j3cot4BTl71OqnawHp4ktuqFyTjhhYy8kBe\n"
-            + "4mliNP36WW7fYXh+f5SZqDQ6rgyoJCOmiUlosb6CM2yUPH3oDtOKg/9Z0iMUcXfQ\n"
-            + "y+bxRKSQmDtiSIS7hwUZmQoo30iAZNygMBLnYyVau3YFan+xyBMCFLa2/pfE0qaU\n"
-            + "QMy67XP8uP7DXlepfc4Lk/qa/2WnAqmuTT2ty9MG+X8M8LuiPuMWfOEx8ICUWB9s\n"
-            + "kCCMWCagS7EUIPhp6AOqjMqEWGOyLmclkGCJABEBAAG0HlRlc3R1c2VyIEcgPHRl\n"
-            + "c3RnQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pFgIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQiptSk+LTK6VSwQf/WnIYkLZoARZIUfH61EDlkUPv8+6G\n"
-            + "1YY3YgFFMjeOKybu47eU3QtATEaKHphvKqFtxdNyEtmti1Zx7Cq2LzReY1KoQQ5E\n"
-            + "OlKeyxVmXAuAqoRWesxuG318rVTrozCqSdKPCHLcC26M5sO+Gd2sKbA4DjoSyfrE\n"
-            + "zEOVS1NA9dtZ7WBMXr8gjH//ob7dvuptSAlADaLYYaJugcmbzkRGRbfiCQHqv30I\n"
-            + "+81d7RAeSx8XS38YEWm2IvBLpiS/d7A/2AQ25SHxf+QMMWt83+uOuEVa9rEOraid\n"
-            + "ZC6T8vnSRu1TKkX/60LnJvAw9tigmedi21O6Gpz3H3uGyjuk9o18+m8dJokBIAQQ\n"
-            + "AQIACgUCTH5xfAMFCngACgkQSp+scDH6SMT42gf9H7K0jp6PF1vD5t90bcjtnP/t\n"
-            + "CkOXgfL3lJK/l0KMkoDzyO5z898PP8IAnAj1veJ2fNPsRP903/3K8kd9/31kBriC\n"
-            + "poTVPWBmeLut16TgSDxAQPDLsBPcKe2VadhszOQwhfmdsUlCXwXcwbiAjweXwKh+\n"
-            + "00UoW1GLnPw0T387ttCjHsLe972SVUPFxb6NUkA7val62qxDKg+6MRcf6tDs8sN8\n"
-            + "orhYgh9VJcI3Iw8qK1wHI0CenNie0U5xEkZ5U6W4lfhnL5sggjoAeVeAVLiQ4eiP\n"
-            + "sFrq4TOYq9qfuThYiRaSuTLXzuWG5NVs7NyXxOGFSkwzXrQsBo+LuPwjSCERLbkB\n"
-            + "DQRMfmkWAQgA1O0I9vfZNSRuYTx++SkJccXXqL4neVWEnQ4Ws9tzfSG0Rch3Gb/d\n"
-            + "+ckDtJhlQOdaayTVX7h5k8tTGx0myg6OjG2UM6i+aTgFAzwGnBh/N3p5tTaJhRCF\n"
-            + "x1IapX0N7ijq6rQPPCISc3CUZhCVBTnp5dk3c0/hNxsyYXlI1AwuoMabygzTFN/c\n"
-            + "b1bXp0UTTVrdN+Sj5hHVDvpxyaljLa77I0V+lI3bCil9VhQ9h/TP4C2iK3ZdXOMb\n"
-            + "uW7ANhd+I9LWulmExZIiD9RIsHvB3bDu32g1847uT+DUynKETbZWlZS0Q93Aly1N\n"
-            + "lBIkvOCVCBt+VatzZ8oBV8vbk5R41W1HywARAQABiQEfBBgBAgAJBQJMfmkWAhsM\n"
-            + "AAoJEIqbUpPi0yul/doH+wR+o6UCdD6OZxGMx7d0a7yDJqQFkFf2DRsJvY2suug0\n"
-            + "CMJZRWiA+hIin5P6Brn/eb5nTdWgzlrHxkvb68YkevHALdOvmrYNQFXbb9uWGgEf\n"
-            + "3qERdI8ayJsSTqYsTqyuh9YVz21kADxTHN3JkJ4evjHpyz0Xbtq+oDADg+uswj1b\n"
-            + "ihHthFif54vNMEIW9rX9T7ufhXKamr4LuGwKTPTxV8gEPW4h4ZoQwFKV2qOjR+su\n"
-            + "tHnuXVL24kTnv8CHXUVzJXVTNz7i7fAJTgWc9drH6Ktp3XHfLDBwzT5/5ZhyxGJk\n"
-            + "Qq2Jm/Q8mNkXi34H2DeQ3VPtjtMLr9JR9pf6ivmvUag=\n"
-            + "=34GE\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOXBEx+aRYBCAC77YjBScTjRFHtZvk0yyAy8KAXopbCdMBQs7S7iidFMMxhs0Uu\n"
-            + "D7GeleyVusLFJfEM0Ul0b0pLgfJx9j3cot4BTl71OqnawHp4ktuqFyTjhhYy8kBe\n"
-            + "4mliNP36WW7fYXh+f5SZqDQ6rgyoJCOmiUlosb6CM2yUPH3oDtOKg/9Z0iMUcXfQ\n"
-            + "y+bxRKSQmDtiSIS7hwUZmQoo30iAZNygMBLnYyVau3YFan+xyBMCFLa2/pfE0qaU\n"
-            + "QMy67XP8uP7DXlepfc4Lk/qa/2WnAqmuTT2ty9MG+X8M8LuiPuMWfOEx8ICUWB9s\n"
-            + "kCCMWCagS7EUIPhp6AOqjMqEWGOyLmclkGCJABEBAAEAB/QJiwZmylg1MkL2y0Pc\n"
-            + "anQ4If//M0J0nXkmn/mNjHZyDQhT7caVkDZ01ygsck9xs3uKKxaP0xbyvqaRIvAB\n"
-            + "REQBzPkFevUlJqERfmOpP4OgCi8WZzbdmqG/WvGKxP/cWBbGVbQ2GVSNpkj+QNeO\n"
-            + "nWoc5unFstbQsEG0hww2/Hz7EppYoBvDrDLY1EPKzr0r6sk1O5gk3VWOqMEJVCh+\n"
-            + "K7EV4pPGmzMrfZQ0jSwRpr0HhzzhDYR7+QUbxr4OS5PoSJDFh0+A5kqFagyupe7A\n"
-            + "96L3Lh7wJBQJsOe5xjOu3lkFp+3vU+Mq7VzO9Fnp9BCwjb4mEjI39bJdGeeOVCWR\n"
-            + "sYEEAMjmftMhIHrjGRlbZVrLcZY8Du4CFQqImb2Tluo/6siIEurVp4F2swZFm7fw\n"
-            + "B2v09GGJ6zKpauJuxlbwo3CFnxbk24W39F/SixZLggLPtNOXdSrLIQrQ1AXu5ucQ\n"
-            + "oCnXS5FaVkD3Rtd53hSMIf2xJiSRKGp/1X9hga/phScud7URBADveDh1oEmwl3gc\n"
-            + "gorhABLYV7cPrARteQRV13tYWcuAZ6WjqNlbbW2mzBE7KTh4bgTzIX0uQ6SZ7bPl\n"
-            + "RmuKQHrdOO9vFGiSf3zDnIg8fhqSyy2SNrC/e7teuaguGCrg5GrP5izBAsiwvXbt\n"
-            + "ST3OG7c8Ky717JGTiUeTJoe4IaET+QP/SB4uQzVTrbXjBNtq1KqL/CT7l2ABnXsn\n"
-            + "psaVwHOMmY/wP+PiazMEDvLInDAu7R8oLNGqYR+7UYmYeAGmWgrc0L3yFVC01tTG\n"
-            + "bk7Yt/V5KRKVO2I9x+2CP0v0EqW4BNOJzbx5TJ5lBFLMTvbviOdsoDXw0S98HIHB\n"
-            + "T1bFFmhVeulCDLQeVGVzdHVzZXIgRyA8dGVzdGdAZXhhbXBsZS5jb20+iQE4BBMB\n"
-            + "AgAiBQJMfmkWAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCKm1KT4tMr\n"
-            + "pVLBB/9achiQtmgBFkhR8frUQOWRQ+/z7obVhjdiAUUyN44rJu7jt5TdC0BMRooe\n"
-            + "mG8qoW3F03IS2a2LVnHsKrYvNF5jUqhBDkQ6Up7LFWZcC4CqhFZ6zG4bfXytVOuj\n"
-            + "MKpJ0o8IctwLbozmw74Z3awpsDgOOhLJ+sTMQ5VLU0D121ntYExevyCMf/+hvt2+\n"
-            + "6m1ICUANothhom6ByZvOREZFt+IJAeq/fQj7zV3tEB5LHxdLfxgRabYi8EumJL93\n"
-            + "sD/YBDblIfF/5Awxa3zf6464RVr2sQ6tqJ1kLpPy+dJG7VMqRf/rQucm8DD22KCZ\n"
-            + "52LbU7oanPcfe4bKO6T2jXz6bx0mnQOYBEx+aRYBCADU7Qj299k1JG5hPH75KQlx\n"
-            + "xdeovid5VYSdDhaz23N9IbRFyHcZv935yQO0mGVA51prJNVfuHmTy1MbHSbKDo6M\n"
-            + "bZQzqL5pOAUDPAacGH83enm1NomFEIXHUhqlfQ3uKOrqtA88IhJzcJRmEJUFOenl\n"
-            + "2TdzT+E3GzJheUjUDC6gxpvKDNMU39xvVtenRRNNWt035KPmEdUO+nHJqWMtrvsj\n"
-            + "RX6UjdsKKX1WFD2H9M/gLaIrdl1c4xu5bsA2F34j0ta6WYTFkiIP1Eiwe8HdsO7f\n"
-            + "aDXzju5P4NTKcoRNtlaVlLRD3cCXLU2UEiS84JUIG35Vq3NnygFXy9uTlHjVbUfL\n"
-            + "ABEBAAEAB/48KLaaNJ+xhJgNMA797crF0uyiOAumG/PqfeMLMQs5xQ6OktuXsl6Q\n"
-            + "pus9mLsu8c7Zq9//efsbt1xFMmDVwPQkmAdB60DVMKc16T1C2CcFcTy25vBG4Mqz\n"
-            + "bK6rqCAJ9JSe+H2/cy78X8gF6FR6VAkSUGN62IxcyfnbkW1yv/hiowZ5pQpGVjBH\n"
-            + "sjfu+6HGZhdJIyzrjnVjTJhXNCodtKq1lQGuL2t3ZB6osOXEsFtsI6lQF2s6QZZd\n"
-            + "MUOpSO+X1Rb5TCpWpR/Yj43sH6Tq7LZWEml9fV4wKe2PQWmFW+L8eZCwbYEz6GgZ\n"
-            + "w2pMoMxxOZJsOMOq4LFs4r9qaNQI+sU1BADZhx42JjqBIUsq0OhQcCizjCbPURNw\n"
-            + "7HRfPV8SQkldzmccVzGwFIKQqAVglNdT9AQefUQzx84CRqmWaROXaypkulOB79gM\n"
-            + "R/C/aXOdWz9/dGJ9fT/gcgq1vg9zt7dPE5QIYlhmNdfQPt6R50bUTXe22N2UYL98\n"
-            + "n1pQrhAdlsbT3QQA+pWPXQE4k3Hm7pwCycM2d4TmOIfB6YiaxjMNsZiepV4bqWPX\n"
-            + "iaHh0gw1f8Av6zmMncQELKRspA8Zrj3ZzB/OvNwfpgpqmjS0LyH4u8fGttm7y3In\n"
-            + "/NxZO33omf5vdB2yptzE6DegtsvS94ux6zp01SuzgCXjQbiSjb/VDL0/A8cD/1sQ\n"
-            + "PQGP1yrhn8aX/HAxgJv8cdI6ZnrSUW+G8RnhX281dl5a9so8APchhqeXspYFX6DJ\n"
-            + "Br6MqNkX69a7jthdLZCxaa3hGInr+A/nPVkNEHhjQ8a/kI+28ChRWndofme10hje\n"
-            + "QISFfGuMf6ULK9uo4d1MzGlstfcNRecizfniKby3SBmJAR8EGAECAAkFAkx+aRYC\n"
-            + "GwwACgkQiptSk+LTK6X92gf7BH6jpQJ0Po5nEYzHt3RrvIMmpAWQV/YNGwm9jay6\n"
-            + "6DQIwllFaID6EiKfk/oGuf95vmdN1aDOWsfGS9vrxiR68cAt06+atg1AVdtv25Ya\n"
-            + "AR/eoRF0jxrImxJOpixOrK6H1hXPbWQAPFMc3cmQnh6+MenLPRdu2r6gMAOD66zC\n"
-            + "PVuKEe2EWJ/ni80wQhb2tf1Pu5+Fcpqavgu4bApM9PFXyAQ9biHhmhDAUpXao6NH\n"
-            + "6y60ee5dUvbiROe/wIddRXMldVM3PuLt8AlOBZz12sfoq2ndcd8sMHDNPn/lmHLE\n"
-            + "YmRCrYmb9DyY2ReLfgfYN5DdU+2O0wuv0lH2l/qK+a9RqA==\n"
-            + "=T1WV\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =34GE
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        lQOXBEx+aRYBCAC77YjBScTjRFHtZvk0yyAy8KAXopbCdMBQs7S7iidFMMxhs0Uu
+        D7GeleyVusLFJfEM0Ul0b0pLgfJx9j3cot4BTl71OqnawHp4ktuqFyTjhhYy8kBe
+        4mliNP36WW7fYXh+f5SZqDQ6rgyoJCOmiUlosb6CM2yUPH3oDtOKg/9Z0iMUcXfQ
+        y+bxRKSQmDtiSIS7hwUZmQoo30iAZNygMBLnYyVau3YFan+xyBMCFLa2/pfE0qaU
+        QMy67XP8uP7DXlepfc4Lk/qa/2WnAqmuTT2ty9MG+X8M8LuiPuMWfOEx8ICUWB9s
+        kCCMWCagS7EUIPhp6AOqjMqEWGOyLmclkGCJABEBAAEAB/QJiwZmylg1MkL2y0Pc
+        anQ4If//M0J0nXkmn/mNjHZyDQhT7caVkDZ01ygsck9xs3uKKxaP0xbyvqaRIvAB
+        REQBzPkFevUlJqERfmOpP4OgCi8WZzbdmqG/WvGKxP/cWBbGVbQ2GVSNpkj+QNeO
+        nWoc5unFstbQsEG0hww2/Hz7EppYoBvDrDLY1EPKzr0r6sk1O5gk3VWOqMEJVCh+
+        K7EV4pPGmzMrfZQ0jSwRpr0HhzzhDYR7+QUbxr4OS5PoSJDFh0+A5kqFagyupe7A
+        96L3Lh7wJBQJsOe5xjOu3lkFp+3vU+Mq7VzO9Fnp9BCwjb4mEjI39bJdGeeOVCWR
+        sYEEAMjmftMhIHrjGRlbZVrLcZY8Du4CFQqImb2Tluo/6siIEurVp4F2swZFm7fw
+        B2v09GGJ6zKpauJuxlbwo3CFnxbk24W39F/SixZLggLPtNOXdSrLIQrQ1AXu5ucQ
+        oCnXS5FaVkD3Rtd53hSMIf2xJiSRKGp/1X9hga/phScud7URBADveDh1oEmwl3gc
+        gorhABLYV7cPrARteQRV13tYWcuAZ6WjqNlbbW2mzBE7KTh4bgTzIX0uQ6SZ7bPl
+        RmuKQHrdOO9vFGiSf3zDnIg8fhqSyy2SNrC/e7teuaguGCrg5GrP5izBAsiwvXbt
+        ST3OG7c8Ky717JGTiUeTJoe4IaET+QP/SB4uQzVTrbXjBNtq1KqL/CT7l2ABnXsn
+        psaVwHOMmY/wP+PiazMEDvLInDAu7R8oLNGqYR+7UYmYeAGmWgrc0L3yFVC01tTG
+        bk7Yt/V5KRKVO2I9x+2CP0v0EqW4BNOJzbx5TJ5lBFLMTvbviOdsoDXw0S98HIHB
+        T1bFFmhVeulCDLQeVGVzdHVzZXIgRyA8dGVzdGdAZXhhbXBsZS5jb20+iQE4BBMB
+        AgAiBQJMfmkWAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCKm1KT4tMr
+        pVLBB/9achiQtmgBFkhR8frUQOWRQ+/z7obVhjdiAUUyN44rJu7jt5TdC0BMRooe
+        mG8qoW3F03IS2a2LVnHsKrYvNF5jUqhBDkQ6Up7LFWZcC4CqhFZ6zG4bfXytVOuj
+        MKpJ0o8IctwLbozmw74Z3awpsDgOOhLJ+sTMQ5VLU0D121ntYExevyCMf/+hvt2+
+        6m1ICUANothhom6ByZvOREZFt+IJAeq/fQj7zV3tEB5LHxdLfxgRabYi8EumJL93
+        sD/YBDblIfF/5Awxa3zf6464RVr2sQ6tqJ1kLpPy+dJG7VMqRf/rQucm8DD22KCZ
+        52LbU7oanPcfe4bKO6T2jXz6bx0mnQOYBEx+aRYBCADU7Qj299k1JG5hPH75KQlx
+        xdeovid5VYSdDhaz23N9IbRFyHcZv935yQO0mGVA51prJNVfuHmTy1MbHSbKDo6M
+        bZQzqL5pOAUDPAacGH83enm1NomFEIXHUhqlfQ3uKOrqtA88IhJzcJRmEJUFOenl
+        2TdzT+E3GzJheUjUDC6gxpvKDNMU39xvVtenRRNNWt035KPmEdUO+nHJqWMtrvsj
+        RX6UjdsKKX1WFD2H9M/gLaIrdl1c4xu5bsA2F34j0ta6WYTFkiIP1Eiwe8HdsO7f
+        aDXzju5P4NTKcoRNtlaVlLRD3cCXLU2UEiS84JUIG35Vq3NnygFXy9uTlHjVbUfL
+        ABEBAAEAB/48KLaaNJ+xhJgNMA797crF0uyiOAumG/PqfeMLMQs5xQ6OktuXsl6Q
+        pus9mLsu8c7Zq9//efsbt1xFMmDVwPQkmAdB60DVMKc16T1C2CcFcTy25vBG4Mqz
+        bK6rqCAJ9JSe+H2/cy78X8gF6FR6VAkSUGN62IxcyfnbkW1yv/hiowZ5pQpGVjBH
+        sjfu+6HGZhdJIyzrjnVjTJhXNCodtKq1lQGuL2t3ZB6osOXEsFtsI6lQF2s6QZZd
+        MUOpSO+X1Rb5TCpWpR/Yj43sH6Tq7LZWEml9fV4wKe2PQWmFW+L8eZCwbYEz6GgZ
+        w2pMoMxxOZJsOMOq4LFs4r9qaNQI+sU1BADZhx42JjqBIUsq0OhQcCizjCbPURNw
+        7HRfPV8SQkldzmccVzGwFIKQqAVglNdT9AQefUQzx84CRqmWaROXaypkulOB79gM
+        R/C/aXOdWz9/dGJ9fT/gcgq1vg9zt7dPE5QIYlhmNdfQPt6R50bUTXe22N2UYL98
+        n1pQrhAdlsbT3QQA+pWPXQE4k3Hm7pwCycM2d4TmOIfB6YiaxjMNsZiepV4bqWPX
+        iaHh0gw1f8Av6zmMncQELKRspA8Zrj3ZzB/OvNwfpgpqmjS0LyH4u8fGttm7y3In
+        /NxZO33omf5vdB2yptzE6DegtsvS94ux6zp01SuzgCXjQbiSjb/VDL0/A8cD/1sQ
+        PQGP1yrhn8aX/HAxgJv8cdI6ZnrSUW+G8RnhX281dl5a9so8APchhqeXspYFX6DJ
+        Br6MqNkX69a7jthdLZCxaa3hGInr+A/nPVkNEHhjQ8a/kI+28ChRWndofme10hje
+        QISFfGuMf6ULK9uo4d1MzGlstfcNRecizfniKby3SBmJAR8EGAECAAkFAkx+aRYC
+        GwwACgkQiptSk+LTK6X92gf7BH6jpQJ0Po5nEYzHt3RrvIMmpAWQV/YNGwm9jay6
+        6DQIwllFaID6EiKfk/oGuf95vmdN1aDOWsfGS9vrxiR68cAt06+atg1AVdtv25Ya
+        AR/eoRF0jxrImxJOpixOrK6H1hXPbWQAPFMc3cmQnh6+MenLPRdu2r6gMAOD66zC
+        PVuKEe2EWJ/ni80wQhb2tf1Pu5+Fcpqavgu4bApM9PFXyAQ9biHhmhDAUpXao6NH
+        6y60ee5dUvbiROe/wIddRXMldVM3PuLt8AlOBZz12sfoq2ndcd8sMHDNPn/lmHLE
+        YmRCrYmb9DyY2ReLfgfYN5DdU+2O0wuv0lH2l/qK+a9RqA==
+        =T1WV
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -744,99 +772,103 @@
    */
   public static TestKey keyH() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx+aSUBCADzpZ1h9awUQR1ChzrMhtoE1ltyTlJpS1G5HFEov9QNxVDTjpB8\n"
-            + "PMdb20NNdk/7g6E+ETpCBJGPoC4/TPFDiqe+UI7cRrRZJVbInkCbflYycLTUt9qW\n"
-            + "5c7IuyZA1+cSYaKp3jYccFZfIWvfTWDLWyUozTs9t1TsI28s3r5fBPvrZ+F1nYv/\n"
-            + "xpSkx3Zsxnn7QJTnd63rZdp0RdfJmF2rXERwR6XVtuLj5CqrFoLxy6OrSOl4am4J\n"
-            + "C2HRWhskB21LpdRtloY8bz0DOn6W6JUFRmSxQ1kbPClOXaiNhzMI0fD/KFnHImgR\n"
-            + "IKbbQyHHsKHBjNyn+zTIm5zUL6JMZMf9PoSZABEBAAG0HlRlc3R1c2VyIEggPHRl\n"
-            + "c3RoQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pJQIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQZcQzxAgOVyORcAf/QaHVlyhlBnU4edujW2uG/PFrZvwK\n"
-            + "fqOKW0QqQ7kVN8okKhnFv4y11IwLIzL9mOLYe2+Zyv3I3bz4X8Xw+MsBF6sMWLLf\n"
-            + "9ieu4Wz/5ScVu0PxY36kgV0AQRiLXk802Vk4t9jElCp9qx/dDln7f3879LLb3wNt\n"
-            + "fajne8EH0hjR4E3joPoG+IXSvSzWcPoZTmAZOKHPcRS8iqy0Ao8/UuQWYCedI/4R\n"
-            + "S1IJaByk8mmkMkqqV0kuPyDkvGpqhfh9zFYh97LuKcJktRTEBp3YMvuGcBDBwofG\n"
-            + "vYIVEMr7Ci5rowRQO/sxJfI1zNSWterWC46v6tOb9IvenOgP0/dQxlU82YkBIAQQ\n"
-            + "AQIACgUCTH5xmAMFAXgACgkQ0CLaOl6a7dCYuQf/V2i3Ih5Dqze0Rz5zoTD56/J7\n"
-            + "0SA4/SFm5eDUirY5B9BohkyxoMVG04uyjUmVs62ree7N0IASmeiF/wkBUZ/r/rr/\n"
-            + "0ntGj43y+1JpuSEohZOfgZJryDKRqyVWhRbeBj0g/SzxIQ1lEt2iHFvdSlfFVd+a\n"
-            + "SH1uDDjT/ZATKfAXcgeajUirWorJRaldue7O4oFe67fMLy36ewvpaMVZ+SpxH4CC\n"
-            + "Owq4Ls3dIAg2C5GQK8G0G7FwT1M26EPg66C79EGYkaxprgrilWE6l7QHc484TY1L\n"
-            + "ys04qKoPRnBinmrRxgRyyimvDN/+nd1jdM6nMe1gVLL3s5Vgo0fJMwNhDZMtdrkB\n"
-            + "DQRMfmklAQgAyajPVMt+OXO1ow7xzb0aZYNa5Xdv+w50JzVeWI0boPOuOmq6RCc1\n"
-            + "3NhOmBzx3CKH6zbSRoLBCZWM3cs1EQbl+8noaxq6YQVWiaROX8U7CThYA50jONP/\n"
-            + "qEk655QFsP8Bq96Z5AT/MflxEMayOtQywUFREF4/olhXvJOdurZfQPGnIis35NUc\n"
-            + "IaubI+gGVsluqWBohLOgqzyF7GMlv+Y2JZE5JKGSTO7ZosyI+OCNdZ6X2CJdDPZ1\n"
-            + "325QHYkmqiMJtb73AYTXurL7NNTxdxQVOnfvwXXW4mgHwPEHr8PU30+2xgo1ktrr\n"
-            + "rpFsd0o2UFhybTe7w1z2sAO1gP5s1bbGlwARAQABiQEfBBgBAgAJBQJMfmklAhsM\n"
-            + "AAoJEGXEM8QIDlcjqkQIAI78nwAgO5EgrUDoFikH6d36Kie9SHleaYcSX2c95Vqc\n"
-            + "umuiSAhaulGX0gM/jwvZkoawSyWIq+O2sPSc9F7VzdYdEnWVj2J5BpVx83TRPrTu\n"
-            + "72tsJ97op6JZz+Q8HwTLYJBmyW3/TEKh+iRL9CBtfTVywodZa58j41vCkx37NFPw\n"
-            + "plglT/Se1/US1rWYTH3Kfqo5zNARLUYzAdcxEpjwXWOvqnybn86KfMwqiOunz8eq\n"
-            + "MnTQYECfUrhX2WrbEAjCSc6/LfrTv/S+cO0rvulO/R97gG99pZdWSUjZypU5KLbp\n"
-            + "MBh0qq2wQxO2iagNXE6ms3kV/XihvCpXo9RArmldmW0=\n"
-            + "=lddL\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx+aSUBCADzpZ1h9awUQR1ChzrMhtoE1ltyTlJpS1G5HFEov9QNxVDTjpB8\n"
-            + "PMdb20NNdk/7g6E+ETpCBJGPoC4/TPFDiqe+UI7cRrRZJVbInkCbflYycLTUt9qW\n"
-            + "5c7IuyZA1+cSYaKp3jYccFZfIWvfTWDLWyUozTs9t1TsI28s3r5fBPvrZ+F1nYv/\n"
-            + "xpSkx3Zsxnn7QJTnd63rZdp0RdfJmF2rXERwR6XVtuLj5CqrFoLxy6OrSOl4am4J\n"
-            + "C2HRWhskB21LpdRtloY8bz0DOn6W6JUFRmSxQ1kbPClOXaiNhzMI0fD/KFnHImgR\n"
-            + "IKbbQyHHsKHBjNyn+zTIm5zUL6JMZMf9PoSZABEBAAEAB/wPPOigp4d9VcwxbLkz\n"
-            + "8OwiONDLz5OuY6hHCjsWMBcgTFqffI9TQc7bExW8ur1KVuNm+RdaaSQ8ZhF2YobF\n"
-            + "SV7v02R36NEfMStiDSmvv+E+stdQZXY9kT5TRgcgr5ATUXllo9DhCvKP7Qxs0Q9Q\n"
-            + "cJEcoedGVxiv0xCBLyYbVbm2sW+GJYjq0R5loaOy/Swbt5vOKQsajU8iyA4czSE8\n"
-            + "Ryr63OtwZ1TZsxekj//HKcngnptYY/FT5TPe4uzw8g1tJTIg/OZXrm8CahWzpfE3\n"
-            + "q8lGafhd0GjLftA9ffIHF0cAUs7HklMrgIKGdVPXfQmPzqDpmH5FO2y6QmqTG0v6\n"
-            + "JYW9BAD4Iobwh80MT3JZhJ0jGYMdi07cRyFN+hRwVKgNcBTdx3QGpGJatcyumD0C\n"
-            + "Yn/aXAn+XUkewSgYhdj9sSRodnWGoavdWELxUQkktsdiFg2/rnqmpqRXTGfR/tDh\n"
-            + "ohD2JaPrsavmUF6ShT3stGp8nUN+n6Bhd+QosaCZm5TC1CtA7QQA+16rrNNdP8XN\n"
-            + "MvpQRqJM5ljH0haqR/yD8vdCCZjk23hBk3YsXwSrhSbPzMeZC2FcDqkQTraTxrSG\n"
-            + "U0+xK3NjKKtbzCjQFH4cy4zdNMUX04OWopLGOEnnvTYukGtXT4lZQ9qm8ZBPh5a4\n"
-            + "cXfWy3ovjvRbxUuFOWm0gOfIoRcuWN0D/isTjqPmjihCuWkKTfa3xoq+dD7ynYhg\n"
-            + "Yu3UKfCqbNVor59ZrB4AkQiaVIDLKim3E1XDMS+IukmTuNVXpJeqK32tAYbEduHM\n"
-            + "7kwEq7SgVh34QvryKjCC/EUkDcjSQ+xlUaKl8QKYOdwtH97zZYK6QixB4uNQ6CuM\n"
-            + "75dqTZ6iQw7jQA+0HlRlc3R1c2VyIEggPHRlc3RoQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTH5pJQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQZcQzxAgO\n"
-            + "VyORcAf/QaHVlyhlBnU4edujW2uG/PFrZvwKfqOKW0QqQ7kVN8okKhnFv4y11IwL\n"
-            + "IzL9mOLYe2+Zyv3I3bz4X8Xw+MsBF6sMWLLf9ieu4Wz/5ScVu0PxY36kgV0AQRiL\n"
-            + "Xk802Vk4t9jElCp9qx/dDln7f3879LLb3wNtfajne8EH0hjR4E3joPoG+IXSvSzW\n"
-            + "cPoZTmAZOKHPcRS8iqy0Ao8/UuQWYCedI/4RS1IJaByk8mmkMkqqV0kuPyDkvGpq\n"
-            + "hfh9zFYh97LuKcJktRTEBp3YMvuGcBDBwofGvYIVEMr7Ci5rowRQO/sxJfI1zNSW\n"
-            + "terWC46v6tOb9IvenOgP0/dQxlU82Z0DmARMfmklAQgAyajPVMt+OXO1ow7xzb0a\n"
-            + "ZYNa5Xdv+w50JzVeWI0boPOuOmq6RCc13NhOmBzx3CKH6zbSRoLBCZWM3cs1EQbl\n"
-            + "+8noaxq6YQVWiaROX8U7CThYA50jONP/qEk655QFsP8Bq96Z5AT/MflxEMayOtQy\n"
-            + "wUFREF4/olhXvJOdurZfQPGnIis35NUcIaubI+gGVsluqWBohLOgqzyF7GMlv+Y2\n"
-            + "JZE5JKGSTO7ZosyI+OCNdZ6X2CJdDPZ1325QHYkmqiMJtb73AYTXurL7NNTxdxQV\n"
-            + "OnfvwXXW4mgHwPEHr8PU30+2xgo1ktrrrpFsd0o2UFhybTe7w1z2sAO1gP5s1bbG\n"
-            + "lwARAQABAAf8C3vFcrqz0Wm5ajOrqV+fZTB5uJ94jP9htengGYLPk/bMcR8qxD7H\n"
-            + "XnAi6Z6cV0DQJKDWkJVZkMYnY2ny96lA53mz9oVrH6NCLkxg+istFXVT7cDBBLdt\n"
-            + "05N3+z/+ovmiirr+YHG4Zowh2Ca4d4kl6sNhbmEvlnsZY++0B7Hi8ru2KgFBag2g\n"
-            + "wDmeVt2+ANJNfJ4uIHUEG+sDSDL4+rxQlBTMhxfVY5+zjbvzPlTf2jyAgDa5zGN2\n"
-            + "vRjB33Z0lbdZTeW7HsJcDsXaS77lKnQeWMmHSvpOXvFSIjnrWpxcMpg8hGY5e5UC\n"
-            + "zLCk+nucY/Od1NbtFYu/e7fl9/n3YnT7AQQA0v/t43Ut3go9vRlb47NN/KpJYL1N\n"
-            + "hh9F/SRzFwWxS+79CiZkf/bgmdJe4XkkS7QJMv+nXhtcko/gfzoaCrvIWIAyvhYa\n"
-            + "7tEbqH+iZ0eaLrQf7bu89Jmp2UNRT1EHLzm38eJ8gg7eNu+SjIhs3wART1KB7GvT\n"
-            + "YmpN5caJA2t2OaEEAPSq7CbvlPDc0qomQSs+NrDnhAv89mQEeksZRmhVa0o4Z7EO\n"
-            + "84DzM+Vxho5fn9h0LtxthhuKWKT8uYN/Qu4Y42cKQuRgMx09+GGwc4GWSC6gJPeP\n"
-            + "oKVJCdZx0l9u8fWQb37gnyH34WDxPvdQx3e4iw/dvruNzu17zmPndkdcyEU3BACD\n"
-            + "yXo21SEflFcfrO16VsITXWc9yweKTSD8Mq7wg2GG6eJPopgtwCLZSlYjnehxD2w2\n"
-            + "38lyr6jGPyITvalVwH6R//676Q2osbQ948Dv2ZcxaTlyla4RyY6E33hsnV9m8ZmM\n"
-            + "PUoNJvFSkKCuPy1N5zaYgUAPKwbEkc3qG+bZm+x2WU2biQEfBBgBAgAJBQJMfmkl\n"
-            + "AhsMAAoJEGXEM8QIDlcjqkQIAI78nwAgO5EgrUDoFikH6d36Kie9SHleaYcSX2c9\n"
-            + "5VqcumuiSAhaulGX0gM/jwvZkoawSyWIq+O2sPSc9F7VzdYdEnWVj2J5BpVx83TR\n"
-            + "PrTu72tsJ97op6JZz+Q8HwTLYJBmyW3/TEKh+iRL9CBtfTVywodZa58j41vCkx37\n"
-            + "NFPwplglT/Se1/US1rWYTH3Kfqo5zNARLUYzAdcxEpjwXWOvqnybn86KfMwqiOun\n"
-            + "z8eqMnTQYECfUrhX2WrbEAjCSc6/LfrTv/S+cO0rvulO/R97gG99pZdWSUjZypU5\n"
-            + "KLbpMBh0qq2wQxO2iagNXE6ms3kV/XihvCpXo9RArmldmW0=\n"
-            + "=voB9\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx+aSUBCADzpZ1h9awUQR1ChzrMhtoE1ltyTlJpS1G5HFEov9QNxVDTjpB8
+        PMdb20NNdk/7g6E+ETpCBJGPoC4/TPFDiqe+UI7cRrRZJVbInkCbflYycLTUt9qW
+        5c7IuyZA1+cSYaKp3jYccFZfIWvfTWDLWyUozTs9t1TsI28s3r5fBPvrZ+F1nYv/
+        xpSkx3Zsxnn7QJTnd63rZdp0RdfJmF2rXERwR6XVtuLj5CqrFoLxy6OrSOl4am4J
+        C2HRWhskB21LpdRtloY8bz0DOn6W6JUFRmSxQ1kbPClOXaiNhzMI0fD/KFnHImgR
+        IKbbQyHHsKHBjNyn+zTIm5zUL6JMZMf9PoSZABEBAAG0HlRlc3R1c2VyIEggPHRl
+        c3RoQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pJQIbAwYLCQgHAwIGFQgCCQoL
+        BBYCAwECHgECF4AACgkQZcQzxAgOVyORcAf/QaHVlyhlBnU4edujW2uG/PFrZvwK
+        fqOKW0QqQ7kVN8okKhnFv4y11IwLIzL9mOLYe2+Zyv3I3bz4X8Xw+MsBF6sMWLLf
+        9ieu4Wz/5ScVu0PxY36kgV0AQRiLXk802Vk4t9jElCp9qx/dDln7f3879LLb3wNt
+        fajne8EH0hjR4E3joPoG+IXSvSzWcPoZTmAZOKHPcRS8iqy0Ao8/UuQWYCedI/4R
+        S1IJaByk8mmkMkqqV0kuPyDkvGpqhfh9zFYh97LuKcJktRTEBp3YMvuGcBDBwofG
+        vYIVEMr7Ci5rowRQO/sxJfI1zNSWterWC46v6tOb9IvenOgP0/dQxlU82YkBIAQQ
+        AQIACgUCTH5xmAMFAXgACgkQ0CLaOl6a7dCYuQf/V2i3Ih5Dqze0Rz5zoTD56/J7
+        0SA4/SFm5eDUirY5B9BohkyxoMVG04uyjUmVs62ree7N0IASmeiF/wkBUZ/r/rr/
+        0ntGj43y+1JpuSEohZOfgZJryDKRqyVWhRbeBj0g/SzxIQ1lEt2iHFvdSlfFVd+a
+        SH1uDDjT/ZATKfAXcgeajUirWorJRaldue7O4oFe67fMLy36ewvpaMVZ+SpxH4CC
+        Owq4Ls3dIAg2C5GQK8G0G7FwT1M26EPg66C79EGYkaxprgrilWE6l7QHc484TY1L
+        ys04qKoPRnBinmrRxgRyyimvDN/+nd1jdM6nMe1gVLL3s5Vgo0fJMwNhDZMtdrkB
+        DQRMfmklAQgAyajPVMt+OXO1ow7xzb0aZYNa5Xdv+w50JzVeWI0boPOuOmq6RCc1
+        3NhOmBzx3CKH6zbSRoLBCZWM3cs1EQbl+8noaxq6YQVWiaROX8U7CThYA50jONP/
+        qEk655QFsP8Bq96Z5AT/MflxEMayOtQywUFREF4/olhXvJOdurZfQPGnIis35NUc
+        IaubI+gGVsluqWBohLOgqzyF7GMlv+Y2JZE5JKGSTO7ZosyI+OCNdZ6X2CJdDPZ1
+        325QHYkmqiMJtb73AYTXurL7NNTxdxQVOnfvwXXW4mgHwPEHr8PU30+2xgo1ktrr
+        rpFsd0o2UFhybTe7w1z2sAO1gP5s1bbGlwARAQABiQEfBBgBAgAJBQJMfmklAhsM
+        AAoJEGXEM8QIDlcjqkQIAI78nwAgO5EgrUDoFikH6d36Kie9SHleaYcSX2c95Vqc
+        umuiSAhaulGX0gM/jwvZkoawSyWIq+O2sPSc9F7VzdYdEnWVj2J5BpVx83TRPrTu
+        72tsJ97op6JZz+Q8HwTLYJBmyW3/TEKh+iRL9CBtfTVywodZa58j41vCkx37NFPw
+        plglT/Se1/US1rWYTH3Kfqo5zNARLUYzAdcxEpjwXWOvqnybn86KfMwqiOunz8eq
+        MnTQYECfUrhX2WrbEAjCSc6/LfrTv/S+cO0rvulO/R97gG99pZdWSUjZypU5KLbp
+        MBh0qq2wQxO2iagNXE6ms3kV/XihvCpXo9RArmldmW0=
+        =lddL
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =voB9
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -845,99 +877,103 @@
    */
   public static TestKey keyI() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx+aTEBCAC6dFperkew4ZowIfEyAjScjPBggcbw5XUXxLCF0nBRjWH+HvuI\n"
-            + "CGwznRyeuTiy5yyB9/CcvTLTkEs8qIyJUJoikm7QpaVVL6imVq1HD1xcOJpV1FV1\n"
-            + "eFu562xCRDUqD6KQf54N04V9TMDyubhPkQYbx1H2gq+uBEo9d1w6AsSMgaUn3xH/\n"
-            + "xYe+INxcP6jFT2OKc36x+8ipP6pc8Hba1X90JwadOcJlwEyJfJKs7hYHTaYn+I6+\n"
-            + "4w0Y//WebhT4ocsYIiOYrENQUcic+vL3fkwwJCloyDBCGxr7w7Gn4Pe3peTCl4Sp\n"
-            + "vIIoYnHPW4h3Nyh8qAlBNDw7dCPS9LP7wRdNABEBAAG0HlRlc3R1c2VyIEkgPHRl\n"
-            + "c3RpQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pMQIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQ0CLaOl6a7dAjNQf/fLmGeKgaesawP53UeioQ8hgDEFKP\n"
-            + "BddNQP248NpReZ1rg3h8Q21PQJVKrtDYn94WJi5NTqUtk9rtx9SiqKaEc3wzIpLc\n"
-            + "nIYrgGLWot5nq+5V1nY9t9QAiJJDrmm2/3tX+jTWW6CpuLih7IsD+gJmpZkY6PfM\n"
-            + "T+teKEeh5E1XBbu10fwDwMJta+043/TiljInjER1f/b41EnSjI6YXFnxnyiLeDgD\n"
-            + "A1QIIzB/W2ccGqphzJriDETDJhKFZIeqvjylZofgCLyMRSyZtsu+b4pfBK3hMpu5\n"
-            + "aaYylaM1BWOpAiqUmGUKqxN/o9EGx4wvsMxK6xgiZe5UdQPaoDcFCsEMg4kBIAQQ\n"
-            + "AQIACgUCTH5xrAMFAXgACgkQoTk8RsLmoZiu2Af8D4PnyWkosYYkcmU4T7CvIHGW\n"
-            + "Qnx4KsnYWaAqYrYrorL6R+f8SZ5caGwj05UOvHnqx/Ij0a1Zv4MpEuzB0se1XkyQ\n"
-            + "eCLdAIKVodfiepsCHyqW6/mc9LV2qKS1HF5x5LwDkI1atOuPt/O14fch4E0beTbl\n"
-            + "FXzGo7YdpH8RunV8l+i3FxxTcUtUkij3Ro4EMwVF/6YG8gBOd08GxWspEQWBH3GK\n"
-            + "k7Repj4IPwXCoEfU1H+XJNPaM5cnt+L87QfbhNOWmHmWhhrOmZg160joODON8w8x\n"
-            + "j3gma9Cp6luPDEQC3XnsEup3BdCdIciG5JS6JA/2GDeulg+eS4x9Xkmmp6nzObkB\n"
-            + "DQRMfmkxAQgAxeT+bUBbADga+lYtkmtYVbuG7uWjwdg9TR6qWKD7n37mcu6OgNNl\n"
-            + "rPaHoClvOL20fcArZ8wT/FbjvDI6ZHn22YA19OvAR+Eqmf3D7qTmebchnCu955Pk\n"
-            + "X7AOOpKfX48qoYq8BoskZDnbFidm5YKfIin3CNDdlQbd3na+ihGCuv0KoGzefuAH\n"
-            + "cITeYEUESh7HLzQ9/pMES9eCgdTEkwYD5NJjfkLnj2kZtDsSiNnENZ0TIlyKOBMn\n"
-            + "ixgsARDjLrkqyTg79thWALiqVBXUKn2NBtMkK5xTDc/7q3nIw4InYMIrLtntSu1w\n"
-            + "pn1gXbdg1HFl5BgqEB9Fp0k02YvrSiiVswARAQABiQEfBBgBAgAJBQJMfmkxAhsM\n"
-            + "AAoJENAi2jpemu3QFPoH/1ynX1j1QWL8TfJFPoB3vXivwGURs3J7LsywHTRjpQVQ\n"
-            + "vxQvKTzB1+woUxtEbdjKgMbvY/ShHSlEZKVV9l3ZihrNewHA1GMHrDtBGXcNRP9B\n"
-            + "RfJHTrDzjUxrEEwu4QIq71o4tS89NvQmlYYi7O4ThtVB4hYSwl436+vAT9ybIQkU\n"
-            + "OjCkYrKye6JHs1K4BnVuWcOVujQwW4H8QFbddcWF49uN6DSqrwDFsjFog6wL7u6V\n"
-            + "UL5upRBP/RZWA4HKJVF2tS0Ptr6xLTmf4Tp5n10CGFYkPcRp9biVyeVRJBW4uZf0\n"
-            + "EDsn9J5rNG0pWtgnhAEi6smoT4fADTOzpOovUiTSQhQ=\n"
-            + "=SiG3\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx+aTEBCAC6dFperkew4ZowIfEyAjScjPBggcbw5XUXxLCF0nBRjWH+HvuI\n"
-            + "CGwznRyeuTiy5yyB9/CcvTLTkEs8qIyJUJoikm7QpaVVL6imVq1HD1xcOJpV1FV1\n"
-            + "eFu562xCRDUqD6KQf54N04V9TMDyubhPkQYbx1H2gq+uBEo9d1w6AsSMgaUn3xH/\n"
-            + "xYe+INxcP6jFT2OKc36x+8ipP6pc8Hba1X90JwadOcJlwEyJfJKs7hYHTaYn+I6+\n"
-            + "4w0Y//WebhT4ocsYIiOYrENQUcic+vL3fkwwJCloyDBCGxr7w7Gn4Pe3peTCl4Sp\n"
-            + "vIIoYnHPW4h3Nyh8qAlBNDw7dCPS9LP7wRdNABEBAAEAB/oCD6EKLvjXgItlqdm/\n"
-            + "X+OWMYHDCtuRCMW7+2gEw/TxfLeGJaOHWxAouwUIArEEb/hjdaRfIg4wdJUxmyPX\n"
-            + "WyNqUdupkjdXNa7RNaesIi0ilrdZOn7NlHWJCCXwKt2R0jd2p8PDED6CWaE1+76I\n"
-            + "/IuwOHDTD8MABke3KvHDXMxjzdeuRbm670Aqz6zTVY+BZG1GH63Ef5JEyezMgAU5\n"
-            + "42+v+OgD0W0/jCxF7jt2ddP9QiOzu0q65mI4qlOuSebxjH8P7ye0LU9EuWVgAcwc\n"
-            + "YJh2lk3eH8bCWTwlIHj4+8MYgY5i510I5xfY3sWuylw/qtFP9vYjisrysadcUExc\n"
-            + "QUxFBADXQSCmvtgRoSLiGfQv2y2qInx67eJw8pUXFEIJKdOFOhX4vogT9qPWQAms\n"
-            + "/vSshcsAPgpZJZ8MNeGpMGLAGm8y4D2zWWd9YLNmVXsPu7EyrDpXlKHCFnsQfOGN\n"
-            + "c5j8u4CHBn1cS/Yk53S+6Yge2jvnOjVNFmxB0ocs0Y5zbdTJYwQA3b+hQebH7NNr\n"
-            + "FlPwthRZS0TiX5+qkE9tE/0mpRrUN3iS9bnF0IXRmHFp7Hz+EsVbA2Re2A5HIHnQ\n"
-            + "/BSpAsSHRhjU3MH4gzwfg9W43eZGVfofSY6IlUCIcd1bGjSAjJgmfhjU7ofS59i/\n"
-            + "DjzP1jBfXdjOEUQULTkXjHPqO7j4048D/jqMwZNY3AawTMjqKr9nGK49aWv/OVdy\n"
-            + "6xGn4dRJNk3gnnIvjAEFy5+HHbUCJ2lA3X2AssQ9tvbuyDnoSL5/G+zEYtyRuAC5\n"
-            + "9TLQQRmy4qjsYC5TwfoUwFbgqRsmGUcjj2wtE+gb1S8P/zudYrEqOD3K60Y5qXcn\n"
-            + "S3PHgJ++5TzFQba0HlRlc3R1c2VyIEkgPHRlc3RpQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTH5pMQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ0CLaOl6a\n"
-            + "7dAjNQf/fLmGeKgaesawP53UeioQ8hgDEFKPBddNQP248NpReZ1rg3h8Q21PQJVK\n"
-            + "rtDYn94WJi5NTqUtk9rtx9SiqKaEc3wzIpLcnIYrgGLWot5nq+5V1nY9t9QAiJJD\n"
-            + "rmm2/3tX+jTWW6CpuLih7IsD+gJmpZkY6PfMT+teKEeh5E1XBbu10fwDwMJta+04\n"
-            + "3/TiljInjER1f/b41EnSjI6YXFnxnyiLeDgDA1QIIzB/W2ccGqphzJriDETDJhKF\n"
-            + "ZIeqvjylZofgCLyMRSyZtsu+b4pfBK3hMpu5aaYylaM1BWOpAiqUmGUKqxN/o9EG\n"
-            + "x4wvsMxK6xgiZe5UdQPaoDcFCsEMg50DmARMfmkxAQgAxeT+bUBbADga+lYtkmtY\n"
-            + "VbuG7uWjwdg9TR6qWKD7n37mcu6OgNNlrPaHoClvOL20fcArZ8wT/FbjvDI6ZHn2\n"
-            + "2YA19OvAR+Eqmf3D7qTmebchnCu955PkX7AOOpKfX48qoYq8BoskZDnbFidm5YKf\n"
-            + "Iin3CNDdlQbd3na+ihGCuv0KoGzefuAHcITeYEUESh7HLzQ9/pMES9eCgdTEkwYD\n"
-            + "5NJjfkLnj2kZtDsSiNnENZ0TIlyKOBMnixgsARDjLrkqyTg79thWALiqVBXUKn2N\n"
-            + "BtMkK5xTDc/7q3nIw4InYMIrLtntSu1wpn1gXbdg1HFl5BgqEB9Fp0k02YvrSiiV\n"
-            + "swARAQABAAf/VXp4O5CUvh9956vZu2kKmt2Jhx9CALT6pZkdU3MVvOr/d517iEHH\n"
-            + "pVJHevLqy8OFdtvO4+LOryyI6f14I3ZbHc+3frdmMqYb1LA8NZScyO5FYkOyn5jO\n"
-            + "CFbvjnVOyeP5MhXO6bSoX3JuI7+ZPoGRYxxlTDWLwJdatoDsBI9TvJhVekyAchTH\n"
-            + "Tyt3NQIvLXqHvKU/8WAgclBKeL/y/idep1BrJ4cIJ+EFp0agEG0WpRRUAYjwfE3P\n"
-            + "aSEV0NOoB8rapPW3XuEjO+ZTht+NYvqgPIdTjwXZGFPYnwvEuz772Th4pO3o/PdF\n"
-            + "2cljvRn3qo+lSVnJ0Ki2pb+LukJSIdfHgQQA1DBdm29a/3dBla2y6wxlSXW/3WBp\n"
-            + "51Vpd8SBuwdVrNNQMwPmf1L93YskJnUKSTo7MwgrYZFWf7QzgfD/cHXr8QK2C1TP\n"
-            + "czUC0/uFCm8pPQoOt/osp3PjDAzGgUAMFXCgLtb04P2JqbFvtse5oTFWrKqmscTG\n"
-            + "KnEBkzfgy37U0iMEAO7BEgXCYvqyztHmQATqJfbpxgQGqk738UW6qWwG8mK6aT5V\n"
-            + "OidZvrWqJ3WeIKmEhoJlY2Ky1ZTuJfeQuVucqzNWlZy2yzDijs+t3v4pFGajv4nV\n"
-            + "ivGvlb/O/QoHBuF/9K36lIIqcZstfa2UIYRqkkdEz2JHWJsr81VvCw2Gb38xA/sG\n"
-            + "hqErrIgSBPRCJObM/gb9rJ6dbA5SNY5trc778EjS1myhyPhGOaOmYbdQMONUqLo2\n"
-            + "q1UZo1G7oaI1Um9v5MXN1yZNX/kvx1TMldZEEixrhCIob81eXSpEUfs+Mz2RqvqT\n"
-            + "YsYquYQNPrPXWZQwTJV6fpsBQUMeE/pmlisaSAijHkXPiQEfBBgBAgAJBQJMfmkx\n"
-            + "AhsMAAoJENAi2jpemu3QFPoH/1ynX1j1QWL8TfJFPoB3vXivwGURs3J7LsywHTRj\n"
-            + "pQVQvxQvKTzB1+woUxtEbdjKgMbvY/ShHSlEZKVV9l3ZihrNewHA1GMHrDtBGXcN\n"
-            + "RP9BRfJHTrDzjUxrEEwu4QIq71o4tS89NvQmlYYi7O4ThtVB4hYSwl436+vAT9yb\n"
-            + "IQkUOjCkYrKye6JHs1K4BnVuWcOVujQwW4H8QFbddcWF49uN6DSqrwDFsjFog6wL\n"
-            + "7u6VUL5upRBP/RZWA4HKJVF2tS0Ptr6xLTmf4Tp5n10CGFYkPcRp9biVyeVRJBW4\n"
-            + "uZf0EDsn9J5rNG0pWtgnhAEi6smoT4fADTOzpOovUiTSQhQ=\n"
-            + "=RcWw\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx+aTEBCAC6dFperkew4ZowIfEyAjScjPBggcbw5XUXxLCF0nBRjWH+HvuI
+        CGwznRyeuTiy5yyB9/CcvTLTkEs8qIyJUJoikm7QpaVVL6imVq1HD1xcOJpV1FV1
+        eFu562xCRDUqD6KQf54N04V9TMDyubhPkQYbx1H2gq+uBEo9d1w6AsSMgaUn3xH/
+        xYe+INxcP6jFT2OKc36x+8ipP6pc8Hba1X90JwadOcJlwEyJfJKs7hYHTaYn+I6+
+        4w0Y//WebhT4ocsYIiOYrENQUcic+vL3fkwwJCloyDBCGxr7w7Gn4Pe3peTCl4Sp
+        vIIoYnHPW4h3Nyh8qAlBNDw7dCPS9LP7wRdNABEBAAG0HlRlc3R1c2VyIEkgPHRl
+        c3RpQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pMQIbAwYLCQgHAwIGFQgCCQoL
+        BBYCAwECHgECF4AACgkQ0CLaOl6a7dAjNQf/fLmGeKgaesawP53UeioQ8hgDEFKP
+        BddNQP248NpReZ1rg3h8Q21PQJVKrtDYn94WJi5NTqUtk9rtx9SiqKaEc3wzIpLc
+        nIYrgGLWot5nq+5V1nY9t9QAiJJDrmm2/3tX+jTWW6CpuLih7IsD+gJmpZkY6PfM
+        T+teKEeh5E1XBbu10fwDwMJta+043/TiljInjER1f/b41EnSjI6YXFnxnyiLeDgD
+        A1QIIzB/W2ccGqphzJriDETDJhKFZIeqvjylZofgCLyMRSyZtsu+b4pfBK3hMpu5
+        aaYylaM1BWOpAiqUmGUKqxN/o9EGx4wvsMxK6xgiZe5UdQPaoDcFCsEMg4kBIAQQ
+        AQIACgUCTH5xrAMFAXgACgkQoTk8RsLmoZiu2Af8D4PnyWkosYYkcmU4T7CvIHGW
+        Qnx4KsnYWaAqYrYrorL6R+f8SZ5caGwj05UOvHnqx/Ij0a1Zv4MpEuzB0se1XkyQ
+        eCLdAIKVodfiepsCHyqW6/mc9LV2qKS1HF5x5LwDkI1atOuPt/O14fch4E0beTbl
+        FXzGo7YdpH8RunV8l+i3FxxTcUtUkij3Ro4EMwVF/6YG8gBOd08GxWspEQWBH3GK
+        k7Repj4IPwXCoEfU1H+XJNPaM5cnt+L87QfbhNOWmHmWhhrOmZg160joODON8w8x
+        j3gma9Cp6luPDEQC3XnsEup3BdCdIciG5JS6JA/2GDeulg+eS4x9Xkmmp6nzObkB
+        DQRMfmkxAQgAxeT+bUBbADga+lYtkmtYVbuG7uWjwdg9TR6qWKD7n37mcu6OgNNl
+        rPaHoClvOL20fcArZ8wT/FbjvDI6ZHn22YA19OvAR+Eqmf3D7qTmebchnCu955Pk
+        X7AOOpKfX48qoYq8BoskZDnbFidm5YKfIin3CNDdlQbd3na+ihGCuv0KoGzefuAH
+        cITeYEUESh7HLzQ9/pMES9eCgdTEkwYD5NJjfkLnj2kZtDsSiNnENZ0TIlyKOBMn
+        ixgsARDjLrkqyTg79thWALiqVBXUKn2NBtMkK5xTDc/7q3nIw4InYMIrLtntSu1w
+        pn1gXbdg1HFl5BgqEB9Fp0k02YvrSiiVswARAQABiQEfBBgBAgAJBQJMfmkxAhsM
+        AAoJENAi2jpemu3QFPoH/1ynX1j1QWL8TfJFPoB3vXivwGURs3J7LsywHTRjpQVQ
+        vxQvKTzB1+woUxtEbdjKgMbvY/ShHSlEZKVV9l3ZihrNewHA1GMHrDtBGXcNRP9B
+        RfJHTrDzjUxrEEwu4QIq71o4tS89NvQmlYYi7O4ThtVB4hYSwl436+vAT9ybIQkU
+        OjCkYrKye6JHs1K4BnVuWcOVujQwW4H8QFbddcWF49uN6DSqrwDFsjFog6wL7u6V
+        UL5upRBP/RZWA4HKJVF2tS0Ptr6xLTmf4Tp5n10CGFYkPcRp9biVyeVRJBW4uZf0
+        EDsn9J5rNG0pWtgnhAEi6smoT4fADTOzpOovUiTSQhQ=
+        =SiG3
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =RcWw
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   /**
@@ -946,93 +982,97 @@
    */
   public static TestKey keyJ() throws Exception {
     return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "mQENBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH\n"
-            + "UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM\n"
-            + "/iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86\n"
-            + "+LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0\n"
-            + "/QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc\n"
-            + "RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAG0HlRlc3R1c2VyIEogPHRl\n"
-            + "c3RqQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoL\n"
-            + "BBYCAwECHgECF4AACgkQoTk8RsLmoZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIs\n"
-            + "XhdxzqdP91UmhVT0df1OBhgTqFkKprBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMO\n"
-            + "TITRPZoFJe3Ezi+HRRPqAPubIcSgeILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bA\n"
-            + "svq+n2jaYUlgL5N6ZNRNakc07e8vH5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB\n"
-            + "0Ah8pl143DFNAq8CfvQCPKwX4WFPkEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8\n"
-            + "Yrue8y9T+j5y699A0GCptb1IKrgxbfhgD//3g3l1eXsEwn2cwFNCt7pZFLkBDQRM\n"
-            + "fmlIAQgA3E2pM6oDJGgfxbqSfykuRtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qR\n"
-            + "qCwL37E4/3nMsZjA7GIFLQj2DrFW3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh\n"
-            + "3RLpbAV6I61NG/wDznW30vmKNJDgPpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAy\n"
-            + "IBLt+piG+bcYKfw9pS8PvXPQMNIi4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2Ydx\n"
-            + "eBxwwxm9sBxF+vhlI+ZEeb9JxGH6jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8\n"
-            + "vcpTSfyHjG2QHc3qG9S/yDCZjhhe2QARAQABiQEfBBgBAgAJBQJMfmlIAhsMAAoJ\n"
-            + "EKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiSZQJjEDo0\n"
-            + "gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8CLXMl0c41\n"
-            + "5FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn3pMi/fcM\n"
-            + "LVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc6dV888xn\n"
-            + "Sew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmtr6eEcl+y\n"
-            + "BkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=\n"
-            + "=ucAX\n"
-            + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-            + "Version: GnuPG v1\n"
-            + "\n"
-            + "lQOYBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH\n"
-            + "UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM\n"
-            + "/iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86\n"
-            + "+LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0\n"
-            + "/QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc\n"
-            + "RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAEAB/9sW1MQR53xKP6yFCeD\n"
-            + "3sdOJlSB1PiMeXgU1JznpTT58CEBdnfdRYVy14qkxM30m8U9gMm88YW8exBscgoZ\n"
-            + "pRnNztNW58phokNPx9AwsRp3p0ETPbZDYI6NDNwuPKQEchn2HEZPvFmjsjPP2hkn\n"
-            + "+Lu8RIUA4uzEFX3bnBxJIP1L2AztqyTgHDfXS4/nqerO/cheXhN7j1TUyRO4hinp\n"
-            + "C3WXaxm2kpQXFP2ktq2eu7YPFoW6I6HzHVDN2Z7fD/NzfmR2h4gcIaSDEjIs893N\n"
-            + "b3hsYiOTYwVFX9TBWLr9rSWyrjR4sWelFuMZpjQ53qq+rBm/+8knoNtoWgZFhbR0\n"
-            + "WJyRBADlBuX8kveqLl31QShgw+6TwTHXI40GiCA6DHwZiTstOO6d2KDNq2nHdtuo\n"
-            + "HBvSKYP4a2na39JKb7YfuSMg16QvxQNd7BQWz+NzbGLQEGuX455OD3TE74ZfVElo\n"
-            + "2H/i51hSjOdWihJVNBGlcDYPgb7oLLTbPdKXxptRM1+wrk2//QQA9s3pw2O3lSbV\n"
-            + "U8JyL/FhdyhDvRDuiNBPnB4O/Ynnzz8YSFwSdSE/u8FpguFWdh+UdSrdwE+Ux8kj\n"
-            + "W/miXaqTxUeKnpzOkiO5O2fLvAeriO3rU9KfBER03+NJo4weSorLXzeU4SWkw63N\n"
-            + "OiY3fc67Nj+l8qi1tmoEJyHUomuy7Q8EAOfBvMzGsQQJ12k+4gOSXN9DTWUa85P6\n"
-            + "IphFHC2cpTDy30IRR55sI6Mf3GpC+KzxEyw7WXjlTensEJAHMpyVVRhv6uF0eMaY\n"
-            + "+QGS+vyCgtUfGIwM5Teu6NjeqyShJDTC8qnM+75JgCNu6gZ2F2iTeY+tM3zE1auq\n"
-            + "po1pUACVm7qwR6u0HlRlc3R1c2VyIEogPHRlc3RqQGV4YW1wbGUuY29tPokBOAQT\n"
-            + "AQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQoTk8RsLm\n"
-            + "oZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIsXhdxzqdP91UmhVT0df1OBhgTqFkK\n"
-            + "prBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMOTITRPZoFJe3Ezi+HRRPqAPubIcSg\n"
-            + "eILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bAsvq+n2jaYUlgL5N6ZNRNakc07e8v\n"
-            + "H5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB0Ah8pl143DFNAq8CfvQCPKwX4WFP\n"
-            + "kEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8Yrue8y9T+j5y699A0GCptb1IKrgx\n"
-            + "bfhgD//3g3l1eXsEwn2cwFNCt7pZFJ0DmARMfmlIAQgA3E2pM6oDJGgfxbqSfyku\n"
-            + "RtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qRqCwL37E4/3nMsZjA7GIFLQj2DrFW\n"
-            + "3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh3RLpbAV6I61NG/wDznW30vmKNJDg\n"
-            + "PpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAyIBLt+piG+bcYKfw9pS8PvXPQMNIi\n"
-            + "4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2YdxeBxwwxm9sBxF+vhlI+ZEeb9JxGH6\n"
-            + "jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8vcpTSfyHjG2QHc3qG9S/yDCZjhhe\n"
-            + "2QARAQABAAf7BUTPxk/u/vi935DpBXoXRKHZnLM3bFuIexCGQ74rQqR2qazUMH8o\n"
-            + "SFEsaBJpm2WyR47J5WqSHNi5SxPT2AUdNFeh/39hxY61Q6SuBFED+WMRbHrKbURR\n"
-            + "WjPiFuwus02eAkAYFWfBFY0n9/BcAhicQa90MTRj+RZb/EHa+GDdbgDatpwEK22z\n"
-            + "pPb3t/D2TC7ModizelngBN7bdp4Vqna/vMLhsiE+FqL+Ob0KiLkDxtcjZljc9xLK\n"
-            + "B7ZuGH/AZfhF08OAxUcsJdu5cF3viBT+HeSI4OUvdfxPFX98U/SFfuW4mPdHPEI9\n"
-            + "438pdjDUIpJFtcnROtZdS2o6C9ohHa5BUwQA52P8AKKRfg7LpaFMvtKkNORnscac\n"
-            + "1qvXLqAXaMeSsvyU5o1GNvSgbhFzDcXbAFJcXdOo2XgT7JzW/6v1uW9AuQPAkYhr\n"
-            + "ep0uE3mewlzWHZR41MQRaMGN4l80RN6ju4c/Ei+OMHYp2DUfZFDBXbxwWpN8tNoR\n"
-            + "S1X+rOL5RsQgkrcEAPO7zthR+GQnIgJC3c9Las9JkPywCxddjoWZoyt6yITVjIso\n"
-            + "IGD0SJppAkOS3Vdb+raydLuN7HmbpPFnvzyc+RdSt+YCGUObrHb/z9MfahzDNG3S\n"
-            + "VwUQEIl+L6glhwscQOCz80MCcYMFMk4TiankvChRFF5Wil//8QnaonH4bcrvA/46\n"
-            + "VB+ZaEdR+Z8IkYIf7oHLJNEwaH+kRTBQ2x5F9Gnwr9SL6AXAkNkvYD4in/+Bw35r\n"
-            + "o9zGirQQvNrvH3JlZ5PWp1/9rRl2Tefaaf8P2ij/Ky2poBLAhPwK56JXHLt5v+BZ\n"
-            + "mQwhY+teJnbfCwiiS0OeWtpVY/tDVU7wYOd2RIhVfkUziQEfBBgBAgAJBQJMfmlI\n"
-            + "AhsMAAoJEKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiS\n"
-            + "ZQJjEDo0gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8C\n"
-            + "LXMl0c415FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn\n"
-            + "3pMi/fcMLVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc\n"
-            + "6dV888xnSew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmt\n"
-            + "r6eEcl+yBkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=\n"
-            + "=NiQI\n"
-            + "-----END PGP PRIVATE KEY BLOCK-----\n");
+        """
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
+        Version: GnuPG v1
+
+        mQENBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH
+        UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM
+        /iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86
+        +LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0
+        /QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc
+        RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAG0HlRlc3R1c2VyIEogPHRl
+        c3RqQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoL
+        BBYCAwECHgECF4AACgkQoTk8RsLmoZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIs
+        XhdxzqdP91UmhVT0df1OBhgTqFkKprBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMO
+        TITRPZoFJe3Ezi+HRRPqAPubIcSgeILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bA
+        svq+n2jaYUlgL5N6ZNRNakc07e8vH5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB
+        0Ah8pl143DFNAq8CfvQCPKwX4WFPkEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8
+        Yrue8y9T+j5y699A0GCptb1IKrgxbfhgD//3g3l1eXsEwn2cwFNCt7pZFLkBDQRM
+        fmlIAQgA3E2pM6oDJGgfxbqSfykuRtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qR
+        qCwL37E4/3nMsZjA7GIFLQj2DrFW3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh
+        3RLpbAV6I61NG/wDznW30vmKNJDgPpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAy
+        IBLt+piG+bcYKfw9pS8PvXPQMNIi4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2Ydx
+        eBxwwxm9sBxF+vhlI+ZEeb9JxGH6jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8
+        vcpTSfyHjG2QHc3qG9S/yDCZjhhe2QARAQABiQEfBBgBAgAJBQJMfmlIAhsMAAoJ
+        EKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiSZQJjEDo0
+        gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8CLXMl0c41
+        5FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn3pMi/fcM
+        LVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc6dV888xn
+        Sew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmtr6eEcl+y
+        BkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=
+        =ucAX
+        -----END PGP PUBLIC KEY BLOCK-----
+        """,
+        """
+        -----BEGIN PGP PRIVATE KEY BLOCK-----
+        Version: GnuPG v1
+
+        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
+        =NiQI
+        -----END PGP PRIVATE KEY BLOCK-----
+        """);
   }
 
   private TestTrustKeys() {}
diff --git a/java/com/google/gerrit/httpd/GetUserFilter.java b/java/com/google/gerrit/httpd/GetUserFilter.java
index 68db98a..637d930 100644
--- a/java/com/google/gerrit/httpd/GetUserFilter.java
+++ b/java/com/google/gerrit/httpd/GetUserFilter.java
@@ -85,8 +85,8 @@
       if (reqEnabled) {
         req.setAttribute(USER_ATTR_KEY, loggableName);
       }
-      if (resEnabled && resp instanceof HttpServletResponse) {
-        ((HttpServletResponse) resp).addHeader(USER_ATTR_KEY, loggableName);
+      if (resEnabled && resp instanceof HttpServletResponse httpServletResponse) {
+        httpServletResponse.addHeader(USER_ATTR_KEY, loggableName);
       }
     }
     chain.doFilter(req, resp);
diff --git a/java/com/google/gerrit/httpd/HtmlDomUtil.java b/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 16e0938..68f7f02 100644
--- a/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -101,8 +101,7 @@
     NodeList list = parent.getChildNodes();
     for (int i = 0; i < list.getLength(); i++) {
       Node n = list.item(i);
-      if (n instanceof Element) {
-        Element e = (Element) n;
+      if (n instanceof Element e) {
         if (name.equals(e.getAttribute("id"))) {
           return e;
         }
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 53f33b5..d799324 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -176,8 +176,8 @@
     final NodeList list = parent.getChildNodes();
     for (int i = 0; i < list.getLength(); i++) {
       final Node n = list.item(i);
-      if (n instanceof Element) {
-        final Element e = (Element) n;
+      if (n instanceof Element element) {
+        final Element e = element;
         if (name.equals(e.getAttribute("class"))) {
           e.setTextContent(value);
         }
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 87c0b03..c24cf01 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -304,15 +304,15 @@
 
     if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
       final MessageExtension ext = authRsp.getExtension(SRegMessage.OPENID_NS_SREG);
-      if (ext instanceof SRegResponse) {
-        sregRsp = (SRegResponse) ext;
+      if (ext instanceof SRegResponse sRegResponse) {
+        sregRsp = sRegResponse;
       }
     }
 
     if (authRsp.hasExtension(AxMessage.OPENID_NS_AX)) {
       final MessageExtension ext = authRsp.getExtension(AxMessage.OPENID_NS_AX);
-      if (ext instanceof FetchResponse) {
-        fetchRsp = (FetchResponse) ext;
+      if (ext instanceof FetchResponse fetchResponse) {
+        fetchRsp = fetchResponse;
       }
     }
 
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index 5063a81..1211539 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -666,8 +666,8 @@
    * @throws ProvisionException if the repo is not DelegateRepository or FileRepository.
    */
   private static Path getRepositoryRoot(Repository repo, Project.NameKey nameKey) {
-    if (repo instanceof DelegateRepository) {
-      return getRepositoryRoot(((DelegateRepository) repo).delegate(), nameKey);
+    if (repo instanceof DelegateRepository delegateRepository) {
+      return getRepositoryRoot(delegateRepository.delegate(), nameKey);
     }
 
     if (repo instanceof FileRepository) {
diff --git a/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java b/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
index 3f59084..693faf6 100644
--- a/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
+++ b/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.cache.Cache;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import java.nio.file.Path;
 
 class DirectoryDocServlet extends DocServlet {
@@ -23,9 +22,8 @@
 
   private final Path doc;
 
-  DirectoryDocServlet(
-      Cache<Path, Resource> cache, Path unpackedWar, ExperimentFeatures experimentFeatures) {
-    super(cache, true, experimentFeatures);
+  DirectoryDocServlet(Cache<Path, Resource> cache, Path unpackedWar) {
+    super(cache, true);
     this.doc = unpackedWar.resolve("Documentation");
   }
 
diff --git a/java/com/google/gerrit/httpd/raw/DocServlet.java b/java/com/google/gerrit/httpd/raw/DocServlet.java
index d5027ba..f44abe0 100644
--- a/java/com/google/gerrit/httpd/raw/DocServlet.java
+++ b/java/com/google/gerrit/httpd/raw/DocServlet.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.httpd.raw;
 
-import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION;
-
 import com.google.common.cache.Cache;
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.Optional;
@@ -28,19 +25,15 @@
 abstract class DocServlet extends ResourceServlet {
   private static final long serialVersionUID = 1L;
 
-  private final ExperimentFeatures experimentFeatures;
-
-  DocServlet(Cache<Path, Resource> cache, boolean refresh, ExperimentFeatures experimentFeatures) {
+  DocServlet(Cache<Path, Resource> cache, boolean refresh) {
     super(cache, refresh);
-    this.experimentFeatures = experimentFeatures;
   }
 
   @Override
   protected boolean shouldProcessResourceBeforeServe(
       HttpServletRequest req, HttpServletResponse rsp, Path p) {
     String nonce = (String) req.getAttribute("nonce");
-    if (!experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
-        || nonce == null) {
+    if (nonce == null) {
       return false;
     }
     return ResourceServlet.contentType(p.toString()).equals("text/html");
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 038d9f2..65c2d2b 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -101,6 +101,7 @@
   private static final String FAVICON_SERVLET = "FaviconServlet";
   private static final String SERVICE_WORKER_SERVLET = "ServiceWorkerServlet";
   private static final String GERRIT_ICON_SERVLET = "GerritIconServlet";
+  private static final String DIFFY_ICON_SERVLET = "DiffyIconServlet";
   private static final String MANIFEST_SERVLET = "ManifestServlet";
   private static final String POLYGERRIT_INDEX_SERVLET = "PolyGerritUiIndexServlet";
   private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
@@ -144,13 +145,12 @@
   @Provides
   @Singleton
   @Named(DOC_SERVLET)
-  HttpServlet getDocServlet(
-      @Named(CACHE) Cache<Path, Resource> cache, ExperimentFeatures experimentFeatures) {
+  HttpServlet getDocServlet(@Named(CACHE) Cache<Path, Resource> cache) {
     Paths p = getPaths();
     if (p.warFs != null) {
-      return new WarDocServlet(cache, p.warFs, experimentFeatures);
+      return new WarDocServlet(cache, p.warFs);
     } else if (p.unpackedWar != null && !p.isDev()) {
-      return new DirectoryDocServlet(cache, p.unpackedWar, experimentFeatures);
+      return new DirectoryDocServlet(cache, p.unpackedWar);
     } else {
       return new HttpServlet() {
         private static final long serialVersionUID = 1L;
@@ -170,6 +170,7 @@
       serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
       serve("/favicon.ico").with(named(FAVICON_SERVLET));
       serve("/gerrit_icon.png").with(named(GERRIT_ICON_SERVLET));
+      serve("/diffy.png").with(named(DIFFY_ICON_SERVLET));
       serve("/service-worker.js").with(named(SERVICE_WORKER_SERVLET));
       serve("/manifest.webmanifest").with(named(MANIFEST_SERVLET));
     }
@@ -219,6 +220,17 @@
 
     @Provides
     @Singleton
+    @Named(DIFFY_ICON_SERVLET)
+    HttpServlet getDiffyIconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+      Paths p = getPaths();
+      if (p.warFs != null) {
+        return new SingleFileServlet(cache, p.warFs.getPath("/diffy.png"), false);
+      }
+      return new SingleFileServlet(cache, webappSourcePath("diffy.png"), true);
+    }
+
+    @Provides
+    @Singleton
     @Named(SERVICE_WORKER_SERVLET)
     HttpServlet getServiceWorkerServlet(@Named(CACHE) Cache<Path, Resource> cache) {
       Paths p = getPaths();
diff --git a/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/java/com/google/gerrit/httpd/raw/WarDocServlet.java
index 718d46d..756ec77 100644
--- a/java/com/google/gerrit/httpd/raw/WarDocServlet.java
+++ b/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.cache.Cache;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
@@ -28,9 +27,8 @@
 
   private final FileSystem warFs;
 
-  WarDocServlet(
-      Cache<Path, Resource> cache, FileSystem warFs, ExperimentFeatures experimentFeatures) {
-    super(cache, false, experimentFeatures);
+  WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) {
+    super(cache, false);
     this.warFs = warFs;
   }
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
index a3aba6d..443f067 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
@@ -62,15 +62,12 @@
     String pathForQuotaReporting = RequestUtil.getRestPathWithoutIds(req);
     // Enrich the quota request we are operating on an interesting collection
     QuotaBackend.WithResource report = quotaBackend.currentUser();
-    if (rsrc instanceof ChangeResource) {
-      ChangeResource changeResource = (ChangeResource) rsrc;
+    if (rsrc instanceof ChangeResource changeResource) {
       report =
           quotaBackend.currentUser().change(changeResource.getId(), changeResource.getProject());
-    } else if (rsrc instanceof AccountResource) {
-      AccountResource accountResource = (AccountResource) rsrc;
+    } else if (rsrc instanceof AccountResource accountResource) {
       report = quotaBackend.currentUser().account(accountResource.getUser().getAccountId());
-    } else if (rsrc instanceof ProjectResource) {
-      ProjectResource projectResource = (ProjectResource) rsrc;
+    } else if (rsrc instanceof ProjectResource projectResource) {
       report = quotaBackend.currentUser().project(projectResource.getNameKey());
     }
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index c336921..e97a540 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -390,8 +390,8 @@
 
         if (path.isEmpty()) {
           globals.quotaChecker.enforce(req);
-          if (rc instanceof NeedsParams) {
-            ((NeedsParams) rc).setParams(qp.params());
+          if (rc instanceof NeedsParams needsParams) {
+            needsParams.setParams(qp.params());
           }
 
           if (isRead(req)) {
@@ -603,16 +603,16 @@
             setXGerritUpdatedRefResponseHeaders(req, res);
           }
 
-          if (response instanceof Response.Redirect) {
+          if (response instanceof Response.Redirect redirect) {
             CacheHeaders.setNotCacheable(res);
-            String location = ((Response.Redirect) response).location();
+            String location = redirect.location();
             res.sendRedirect(location);
             logger.atFinest().log("REST call redirected to: %s", location);
             return;
-          } else if (response instanceof Response.Accepted) {
+          } else if (response instanceof Response.Accepted accepted) {
             CacheHeaders.setNotCacheable(res);
             res.setStatus(response.statusCode());
-            res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
+            res.setHeader(HttpHeaders.LOCATION, accepted.location());
             logger.atFinest().log("REST call succeeded: %d", response.statusCode());
             return;
           }
@@ -626,8 +626,8 @@
 
         if (!identity().equivalent(response, Response.none())) {
           Object value = Response.unwrap(response);
-          if (value instanceof BinaryResult) {
-            responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
+          if (value instanceof BinaryResult binaryResult) {
+            responseBytes = replyBinaryResult(req, res, binaryResult);
           } else {
             responseBytes = replyJson(req, res, false, qp.config(), value);
           }
@@ -996,8 +996,8 @@
       return false;
     }
 
-    if (rsrc instanceof RestResource.HasLastModified) {
-      Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
+    if (rsrc instanceof RestResource.HasLastModified hasLastModified) {
+      Timestamp m = hasLastModified.getLastModified();
       long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
 
       // HTTP times are in seconds, database may have millisecond precision.
@@ -1051,10 +1051,8 @@
   }
 
   private void addResourceStateHeaders(HttpServletResponse res, RestResource rsrc) {
-    if (rsrc instanceof RestResource.HasLastModified) {
-      res.setDateHeader(
-          HttpHeaders.LAST_MODIFIED,
-          ((RestResource.HasLastModified) rsrc).getLastModified().getTime());
+    if (rsrc instanceof RestResource.HasLastModified hasLastModified) {
+      res.setDateHeader(HttpHeaders.LAST_MODIFIED, hasLastModified.getLastModified().getTime());
     }
   }
 
@@ -1174,10 +1172,9 @@
     return (len <= 0 && type == null) || (len == 0 && isType(FORM_TYPE, type));
   }
 
-  @SuppressWarnings("rawtypes")
   private static boolean acceptsRawInput(Type type) {
-    if (type instanceof Class) {
-      for (Field f : ((Class) type).getDeclaredFields()) {
+    if (type instanceof Class<?> cls) {
+      for (Field f : cls.getDeclaredFields()) {
         if (f.getType() == RawInput.class) {
           return true;
         }
@@ -1237,18 +1234,17 @@
           InstantiationException,
           IllegalAccessException,
           InvocationTargetException {
-    if (type instanceof Class) {
-      Class<?> clazz = (Class<?>) type;
+    if (type instanceof Class<?> clazz) {
       Constructor<?> c = clazz.getDeclaredConstructor();
       c.setAccessible(true);
       return c.newInstance();
     }
-    if (type instanceof ParameterizedType) {
-      Type rawType = ((ParameterizedType) type).getRawType();
-      if (rawType instanceof Class && List.class.isAssignableFrom((Class<?>) rawType)) {
+    if (type instanceof ParameterizedType parameterizedType) {
+      Type rawType = parameterizedType.getRawType();
+      if (rawType instanceof Class<?> clazz && List.class.isAssignableFrom(clazz)) {
         return new ArrayList<>();
       }
-      if (rawType instanceof Class && Map.class.isAssignableFrom((Class<?>) rawType)) {
+      if (rawType instanceof Class<?> clazz && Map.class.isAssignableFrom(clazz)) {
         return new HashMap<>();
       }
     }
@@ -1278,8 +1274,8 @@
     buf.write(JSON_MAGIC);
     Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
     Gson gson = newGson(config);
-    if (result instanceof JsonElement) {
-      gson.toJson((JsonElement) result, w);
+    if (result instanceof JsonElement jsonElement) {
+      gson.toJson(jsonElement, w);
     } else {
       gson.toJson(result, w);
     }
diff --git a/java/com/google/gerrit/index/query/AndCardinalPredicate.java b/java/com/google/gerrit/index/query/AndCardinalPredicate.java
index cf0e8c3..aea714f 100644
--- a/java/com/google/gerrit/index/query/AndCardinalPredicate.java
+++ b/java/com/google/gerrit/index/query/AndCardinalPredicate.java
@@ -29,8 +29,8 @@
     }
     int minCardinality = Integer.MAX_VALUE;
     for (Predicate<T> child : getChildren()) {
-      if (child instanceof HasCardinality) {
-        minCardinality = Math.min(((HasCardinality) child).getCardinality(), minCardinality);
+      if (child instanceof HasCardinality hasCardinality) {
+        minCardinality = Math.min(hasCardinality.getCardinality(), minCardinality);
       }
     }
     cardinality = minCardinality;
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 70ab09c..2d995e5 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -130,9 +130,7 @@
       cmp = a.estimateCost() - b.estimateCost();
     }
 
-    if (cmp == 0 && a instanceof DataSource) {
-      DataSource<?> as = (DataSource<?>) a;
-      DataSource<?> bs = (DataSource<?>) b;
+    if (cmp == 0 && a instanceof DataSource<?> as && b instanceof DataSource<?> bs) {
       cmp = as.getCardinality() - bs.getCardinality();
     }
     return cmp;
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 89cfcab..dc8f2e6e 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -41,8 +41,7 @@
     Predicate<T> selectedSource = null;
     int minCardinality = Integer.MAX_VALUE;
     for (Predicate<T> p : getChildren()) {
-      if (p instanceof DataSource) {
-        DataSource<?> source = (DataSource<?>) p;
+      if (p instanceof DataSource<?> source) {
         int cardinality = source.getCardinality();
 
         if (selectedSource == null
diff --git a/java/com/google/gerrit/index/query/FilteredSource.java b/java/com/google/gerrit/index/query/FilteredSource.java
index 95e6435..bbf7ca4 100644
--- a/java/com/google/gerrit/index/query/FilteredSource.java
+++ b/java/com/google/gerrit/index/query/FilteredSource.java
@@ -62,10 +62,9 @@
             pageResultSize++;
           }
 
-          if (last != null && source instanceof Paginated) {
+          if (last != null && source instanceof Paginated p) {
             // Restart source and continue if we have not filled the
             // full limit the caller wants.
-            Paginated<T> p = (Paginated<T>) source;
             QueryOptions opts = p.getOptions();
             final int limit = opts.limit();
             int nextStart = pageResultSize;
diff --git a/java/com/google/gerrit/index/query/OrCardinalPredicate.java b/java/com/google/gerrit/index/query/OrCardinalPredicate.java
index 9d7913a..b658493 100644
--- a/java/com/google/gerrit/index/query/OrCardinalPredicate.java
+++ b/java/com/google/gerrit/index/query/OrCardinalPredicate.java
@@ -29,8 +29,8 @@
     }
     int aggregateCardinality = 0;
     for (Predicate<T> p : getChildren()) {
-      if (p instanceof HasCardinality) {
-        aggregateCardinality += ((HasCardinality) p).getCardinality();
+      if (p instanceof HasCardinality hasCardinality) {
+        aggregateCardinality += hasCardinality.getCardinality();
       }
     }
     cardinality = aggregateCardinality;
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
index 377ace4..3077907 100644
--- a/java/com/google/gerrit/index/query/QueryBuilder.java
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -175,8 +175,8 @@
   @SuppressWarnings("unchecked")
   public static <T, P extends OperatorPredicate<T>> P find(
       Predicate<T> p, Class<P> clazz, String name) {
-    if (p instanceof OperatorPredicate
-        && ((OperatorPredicate<?>) p).getOperator().equals(name)
+    if (p instanceof OperatorPredicate<?> operatorPredicate
+        && operatorPredicate.getOperator().equals(name)
         && clazz.isAssignableFrom(p.getClass())) {
       return (P) p;
     }
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 32399d3..d48a876 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -44,7 +44,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.MergeabilityComputationBehavior;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.IndexDir;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -54,7 +54,6 @@
 import com.google.gerrit.server.query.change.ChangePredicates;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.nio.file.Path;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -76,10 +75,10 @@
   private final Schema<V> schema;
 
   /**
-   * The index directory (config files) is used to signal that an index is ready. This
-   * implementation is consistent with other index backends.
+   * SitePaths (config files) are used to signal that an index is ready. This implementation is
+   * consistent with other index backends.
    */
-  private final Path indexDir;
+  private final SitePaths sitePaths;
 
   private final String indexName;
   private final Map<K, D> indexedDocuments;
@@ -87,9 +86,9 @@
   private int flushAndCommitCount;
   private List<Integer> resultsSizes;
 
-  AbstractFakeIndex(Schema<V> schema, Path indexDir, String indexName) {
+  AbstractFakeIndex(Schema<V> schema, SitePaths sitePaths, String indexName) {
     this.schema = schema;
-    this.indexDir = indexDir;
+    this.sitePaths = sitePaths;
     this.indexName = indexName;
     this.indexedDocuments = new HashMap<>();
     this.queryCount = 0;
@@ -239,7 +238,7 @@
 
   @Override
   public void markReady(boolean ready) {
-    IndexUtils.setReady(indexDir, indexName, schema.getVersion(), ready);
+    IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
   }
 
   /** Method to get a key from a document. */
@@ -271,12 +270,12 @@
     @Inject
     @VisibleForTesting
     protected FakeChangeIndex(
-        @IndexDir Path indexDir,
+        SitePaths sitePaths,
         ChangeData.Factory changeDataFactory,
         @Assisted Schema<ChangeData> schema,
         @GerritServerConfig Config cfg,
         IndexConfig indexConfig) {
-      super(schema, indexDir, "changes");
+      super(schema, sitePaths, "changes");
       this.changeDataFactory = changeDataFactory;
       this.skipMergable = !MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex();
       this.indexConfig = indexConfig;
@@ -357,8 +356,8 @@
   public static class FakeAccountIndex
       extends AbstractFakeIndex<Account.Id, AccountState, AccountState> implements AccountIndex {
     @Inject
-    FakeAccountIndex(@IndexDir Path indexDir, @Assisted Schema<AccountState> schema) {
-      super(schema, indexDir, "accounts");
+    FakeAccountIndex(SitePaths sitePaths, @Assisted Schema<AccountState> schema) {
+      super(schema, sitePaths, "accounts");
     }
 
     @Override
@@ -395,8 +394,8 @@
       extends AbstractFakeIndex<AccountGroup.UUID, InternalGroup, InternalGroup>
       implements GroupIndex {
     @Inject
-    FakeGroupIndex(@IndexDir Path indexDir, @Assisted Schema<InternalGroup> schema) {
-      super(schema, indexDir, "groups");
+    FakeGroupIndex(SitePaths sitePaths, @Assisted Schema<InternalGroup> schema) {
+      super(schema, sitePaths, "groups");
     }
 
     @Override
@@ -432,8 +431,8 @@
   public static class FakeProjectIndex
       extends AbstractFakeIndex<Project.NameKey, ProjectData, ProjectData> implements ProjectIndex {
     @Inject
-    FakeProjectIndex(@IndexDir Path indexDir, @Assisted Schema<ProjectData> schema) {
-      super(schema, indexDir, "projects");
+    FakeProjectIndex(SitePaths sitePaths, @Assisted Schema<ProjectData> schema) {
+      super(schema, sitePaths, "projects");
     }
 
     @Override
diff --git a/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
index 1e8effd..6adb007 100644
--- a/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
+++ b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
@@ -21,13 +21,11 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.GerritIndexStatus;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.nio.file.Path;
 import java.util.Collection;
 import java.util.TreeMap;
 import org.eclipse.jgit.lib.Config;
@@ -40,12 +38,10 @@
   FakeIndexVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs) {
     super(
         sitePaths,
-        indexDir,
         listeners,
         defs,
         VersionManager.shouldPerformOnlineUpgrade(cfg),
diff --git a/java/com/google/gerrit/json/OptionalTypeAdapter.java b/java/com/google/gerrit/json/OptionalTypeAdapter.java
index 9bfa72d..36cc5643 100644
--- a/java/com/google/gerrit/json/OptionalTypeAdapter.java
+++ b/java/com/google/gerrit/json/OptionalTypeAdapter.java
@@ -56,8 +56,8 @@
     // <?> type
     ParameterizedType parameterizedType =
         (ParameterizedType) new TypeLiteral<Optional<?>>() {}.getType();
-    if (typeOfT instanceof ParameterizedType) {
-      parameterizedType = (ParameterizedType) typeOfT;
+    if (typeOfT instanceof ParameterizedType pt) {
+      parameterizedType = pt;
       if (parameterizedType.getActualTypeArguments().length != 1) {
         throw new JsonParseException("Expected one parameter type in Optional.");
       }
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 79205ed..f8845d3 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -251,16 +251,16 @@
             main.invoke(clazz.getConstructor(new Class<?>[] {}).newInstance(), new Object[] {argv});
       }
     } catch (InvocationTargetException ite) {
-      if (ite.getCause() instanceof Exception) {
-        throw (Exception) ite.getCause();
-      } else if (ite.getCause() instanceof Error) {
-        throw (Error) ite.getCause();
+      if (ite.getCause() instanceof Exception e) {
+        throw e;
+      } else if (ite.getCause() instanceof Error e) {
+        throw e;
       } else {
         throw ite;
       }
     }
-    if (res instanceof Number) {
-      return ((Number) res).intValue();
+    if (res instanceof Number number) {
+      return number.intValue();
     }
     return 0;
   }
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 56e0793..9489181 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.index.query.ListResultSet;
 import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.proto.Protos;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.options.AutoFlush;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
@@ -104,7 +105,7 @@
   }
 
   private final Schema<V> schema;
-  private final Path indexDir;
+  private final SitePaths sitePaths;
   private final Directory dir;
   private final String name;
   private final ImmutableSet<String> skipFields;
@@ -120,7 +121,7 @@
   @SuppressWarnings("ThreadPriorityCheck")
   AbstractLuceneIndex(
       Schema<V> schema,
-      Path indexDir,
+      SitePaths sitePaths,
       Directory dir,
       String name,
       ImmutableSet<String> skipFields,
@@ -131,7 +132,7 @@
       Function<V, K> valueToKeyFunction)
       throws IOException {
     this.schema = schema;
-    this.indexDir = indexDir;
+    this.sitePaths = sitePaths;
     this.dir = dir;
     this.name = name;
     this.skipFields = skipFields;
@@ -246,7 +247,7 @@
 
   @Override
   public void markReady(boolean ready) {
-    IndexUtils.setReady(indexDir, name, schema.getVersion(), ready);
+    IndexUtils.setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
   @Override
@@ -554,12 +555,9 @@
     IndexCommit commit = snapshooter.snapshot();
     try {
       Path sourceDir = canonical(((FSDirectory) commit.getDirectory()).getDirectory());
-      Path canonicalIndexDir = canonical(indexDir);
+      Path indexDir = canonical(sitePaths.index_dir);
       Path targetDir =
-          canonicalIndexDir
-              .resolve("snapshots")
-              .resolve(id)
-              .resolve(canonicalIndexDir.relativize(sourceDir));
+          indexDir.resolve("snapshots").resolve(id).resolve(indexDir.relativize(sourceDir));
       if (targetDir.toFile().exists()) {
         throw new FileAlreadyExistsException(targetDir.toString());
       }
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index fa6373e..0603ada 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.options.AutoFlush;
@@ -49,7 +50,7 @@
     implements ChangeIndex {
   ChangeSubIndex(
       Schema<ChangeData> schema,
-      Path indexDir,
+      SitePaths sitePaths,
       Config cfg,
       Path path,
       ImmutableSet<String> skipFields,
@@ -59,7 +60,7 @@
       throws IOException {
     this(
         schema,
-        indexDir,
+        sitePaths,
         LuceneDirectory.open(cfg, path),
         path.getFileName().toString(),
         skipFields,
@@ -70,7 +71,7 @@
 
   ChangeSubIndex(
       Schema<ChangeData> schema,
-      Path indexDir,
+      SitePaths sitePaths,
       Directory dir,
       String subIndex,
       ImmutableSet<String> skipFields,
@@ -80,7 +81,7 @@
       throws IOException {
     super(
         schema,
-        indexDir,
+        sitePaths,
         dir,
         NAME,
         skipFields,
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 4d15916..f7e7e49 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.options.AutoFlush;
@@ -83,28 +82,27 @@
   private final QueryBuilder<AccountState> queryBuilder;
   private final Provider<AccountCache> accountCache;
 
-  private static Directory dir(Schema<AccountState> schema, Config cfg, Path indexDir)
+  private static Directory dir(Schema<AccountState> schema, Config cfg, SitePaths sitePaths)
       throws IOException {
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new ByteBuffersDirectory();
     }
-    Path dir = LuceneVersionManager.getDir(indexDir, ACCOUNTS, schema);
-    return LuceneDirectory.open(cfg, dir);
+    Path indexDir = LuceneVersionManager.getDir(sitePaths, ACCOUNTS, schema);
+    return LuceneDirectory.open(cfg, indexDir);
   }
 
   @Inject
   LuceneAccountIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       Provider<AccountCache> accountCache,
       @Assisted Schema<AccountState> schema,
       AutoFlush autoFlush)
       throws IOException {
     super(
         schema,
-        indexDir,
-        dir(schema, cfg, indexDir),
+        sitePaths,
+        dir(schema, cfg, sitePaths),
         ACCOUNTS,
         ImmutableSet.of(),
         null,
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index fd59370..ebc1062 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -53,7 +53,6 @@
 import com.google.gerrit.server.change.MergeabilityComputationBehavior;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -135,7 +134,6 @@
   LuceneChangeIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       @IndexExecutor(INTERACTIVE) ListeningExecutorService executor,
       ChangeData.Factory changeDataFactory,
       @Assisted Schema<ChangeData> schema,
@@ -161,7 +159,7 @@
       openIndex =
           new ChangeSubIndex(
               schema,
-              indexDir,
+              sitePaths,
               new ByteBuffersDirectory(),
               "ramOpen",
               skipFields,
@@ -171,7 +169,7 @@
       closedIndex =
           new ChangeSubIndex(
               schema,
-              indexDir,
+              sitePaths,
               new ByteBuffersDirectory(),
               "ramClosed",
               skipFields,
@@ -179,11 +177,11 @@
               searcherFactory,
               autoFlush);
     } else {
-      Path dir = LuceneVersionManager.getDir(indexDir, CHANGES, schema);
+      Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES, schema);
       openIndex =
           new ChangeSubIndex(
               schema,
-              indexDir,
+              sitePaths,
               cfg,
               dir.resolve(CHANGES_OPEN),
               skipFields,
@@ -193,7 +191,7 @@
       closedIndex =
           new ChangeSubIndex(
               schema,
-              indexDir,
+              sitePaths,
               cfg,
               dir.resolve(CHANGES_CLOSED),
               skipFields,
@@ -353,8 +351,8 @@
 
     @Override
     public int getCardinality() {
-      if (predicate instanceof HasCardinality) {
-        return ((HasCardinality) predicate).getCardinality();
+      if (predicate instanceof HasCardinality hasCardinality) {
+        return hasCardinality.getCardinality();
       }
       return 10;
     }
@@ -516,10 +514,9 @@
     @Nullable
     private ScoreDoc getSearchAfter(ChangeSubIndex subIndex) {
       if (isSearchAfterPagination
-          && opts.searchAfter() != null
-          && opts.searchAfter() instanceof Map
-          && ((Map<?, ?>) opts.searchAfter()).get(subIndex) instanceof ScoreDoc) {
-        return (ScoreDoc) ((Map<?, ?>) opts.searchAfter()).get(subIndex);
+          && opts.searchAfter() instanceof Map<?, ?> searchAfter
+          && searchAfter.get(subIndex) instanceof ScoreDoc scoreDoc) {
+        return scoreDoc;
       }
       return null;
     }
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 83c9926..54f8ce5 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.options.AutoFlush;
@@ -73,27 +72,27 @@
   private final QueryBuilder<InternalGroup> queryBuilder;
   private final Provider<GroupCache> groupCache;
 
-  private static Directory dir(Schema<?> schema, Config cfg, Path indexDir) throws IOException {
+  private static Directory dir(Schema<?> schema, Config cfg, SitePaths sitePaths)
+      throws IOException {
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new ByteBuffersDirectory();
     }
-    Path dir = LuceneVersionManager.getDir(indexDir, GROUPS, schema);
-    return LuceneDirectory.open(cfg, dir);
+    Path indexDir = LuceneVersionManager.getDir(sitePaths, GROUPS, schema);
+    return LuceneDirectory.open(cfg, indexDir);
   }
 
   @Inject
   LuceneGroupIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       Provider<GroupCache> groupCache,
       @Assisted Schema<InternalGroup> schema,
       AutoFlush autoFlush)
       throws IOException {
     super(
         schema,
-        indexDir,
-        dir(schema, cfg, indexDir),
+        sitePaths,
+        dir(schema, cfg, sitePaths),
         GROUPS,
         ImmutableSet.of(),
         null,
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index f1cbeed..f0d963b 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.options.AutoFlush;
 import com.google.gerrit.server.project.ProjectCache;
@@ -73,28 +72,27 @@
   private final QueryBuilder<ProjectData> queryBuilder;
   private final Provider<ProjectCache> projectCache;
 
-  private static Directory dir(Schema<ProjectData> schema, Config cfg, Path indexDir)
+  private static Directory dir(Schema<ProjectData> schema, Config cfg, SitePaths sitePaths)
       throws IOException {
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new ByteBuffersDirectory();
     }
-    Path dir = LuceneVersionManager.getDir(indexDir, PROJECTS, schema);
-    return LuceneDirectory.open(cfg, dir);
+    Path indexDir = LuceneVersionManager.getDir(sitePaths, PROJECTS, schema);
+    return LuceneDirectory.open(cfg, indexDir);
   }
 
   @Inject
   LuceneProjectIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       Provider<ProjectCache> projectCache,
       @Assisted Schema<ProjectData> schema,
       AutoFlush autoFlush)
       throws IOException {
     super(
         schema,
-        indexDir,
-        dir(schema, cfg, indexDir),
+        sitePaths,
+        dir(schema, cfg, sitePaths),
         PROJECTS,
         ImmutableSet.of(),
         null,
diff --git a/java/com/google/gerrit/lucene/LuceneVersionManager.java b/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 7461ebe..f900d75 100644
--- a/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.GerritIndexStatus;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -40,20 +39,18 @@
 public class LuceneVersionManager extends VersionManager {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  static Path getDir(Path indexDir, String name, Schema<?> schema) {
-    return indexDir.resolve(String.format("%s_%04d", name, schema.getVersion()));
+  static Path getDir(SitePaths sitePaths, String name, Schema<?> schema) {
+    return sitePaths.index_dir.resolve(String.format("%s_%04d", name, schema.getVersion()));
   }
 
   @Inject
   LuceneVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexDir Path indexDir,
       PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs) {
     super(
         sitePaths,
-        indexDir,
         listeners,
         defs,
         VersionManager.shouldPerformOnlineUpgrade(cfg),
@@ -66,7 +63,7 @@
     TreeMap<Integer, VersionManager.Version<V>> versions = new TreeMap<>();
     for (Schema<V> schema : def.getSchemas().values()) {
       // This part is Lucene-specific.
-      Path p = getDir(indexDir, def.getName(), schema);
+      Path p = getDir(sitePaths, def.getName(), schema);
       boolean isDir = Files.isDirectory(p);
       if (Files.exists(p) && !isDir) {
         logger.atWarning().log("Not a directory: %s", p.toAbsolutePath());
@@ -76,7 +73,7 @@
     }
 
     String prefix = def.getName() + "_";
-    try (DirectoryStream<Path> paths = Files.newDirectoryStream(indexDir)) {
+    try (DirectoryStream<Path> paths = Files.newDirectoryStream(sitePaths.index_dir)) {
       for (Path p : paths) {
         String n = p.getFileName().toString();
         if (!n.startsWith(prefix)) {
@@ -93,7 +90,7 @@
         }
       }
     } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Error scanning index directory: %s", indexDir);
+      logger.atSevere().withCause(e).log("Error scanning index directory: %s", sitePaths.index_dir);
     }
     return versions;
   }
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 467a329..3eb18bb 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.index.query.RegexPredicate;
 import com.google.gerrit.index.query.TimestampRangePredicate;
 import java.util.List;
+import java.util.Objects;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.document.IntPoint;
 import org.apache.lucene.document.LongPoint;
@@ -76,24 +77,19 @@
   }
 
   public Query toQuery(Predicate<V> p) throws QueryParseException {
-    if (p instanceof AndPredicate) {
-      return and(p);
-    } else if (p instanceof OrPredicate) {
-      return or(p);
-    } else if (p instanceof NotPredicate) {
-      return not(p);
-    } else if (p instanceof Predicate.Any) {
-      return new MatchAllDocsQuery();
-    } else if (p instanceof IndexPredicate) {
-      return fieldQuery((IndexPredicate<V>) p);
-    } else if (p instanceof PostFilterPredicate) {
-      return new MatchAllDocsQuery();
-    } else {
-      throw new QueryParseException("cannot create query for index: " + p);
-    }
+    Objects.requireNonNull(p, "p must not be null");
+    return switch (p) {
+      case AndPredicate<V> a -> and(a);
+      case OrPredicate<V> o -> or(o);
+      case NotPredicate<V> n -> not(n);
+      case Predicate.Any<V> _ -> new MatchAllDocsQuery();
+      case IndexPredicate<V> ip -> fieldQuery(ip);
+      case PostFilterPredicate<V> _ -> new MatchAllDocsQuery();
+      default -> throw new QueryParseException("cannot create query for index: " + p);
+    };
   }
 
-  private Query or(Predicate<V> p) throws QueryParseException {
+  private Query or(OrPredicate<V> p) throws QueryParseException {
     try {
       BooleanQuery.Builder q = new BooleanQuery.Builder();
       for (int i = 0; i < p.getChildCount(); i++) {
@@ -105,16 +101,16 @@
     }
   }
 
-  private Query and(Predicate<V> p) throws QueryParseException {
+  private Query and(AndPredicate<V> p) throws QueryParseException {
     try {
       BooleanQuery.Builder b = new BooleanQuery.Builder();
       List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
       for (int i = 0; i < p.getChildCount(); i++) {
         Predicate<V> c = p.getChild(i);
-        if (c instanceof NotPredicate) {
-          Predicate<V> n = c.getChild(0);
-          if (n instanceof TimestampRangePredicate) {
-            b.add(notTimestamp((TimestampRangePredicate<V>) n), MUST);
+        if (c instanceof NotPredicate<V> notP) {
+          Predicate<V> n = notP.getChild(0);
+          if (n instanceof TimestampRangePredicate<V> tr) {
+            b.add(notTimestamp(tr), MUST);
           } else {
             not.add(toQuery(n));
           }
@@ -131,10 +127,10 @@
     }
   }
 
-  private Query not(Predicate<V> p) throws QueryParseException {
+  private Query not(NotPredicate<V> p) throws QueryParseException {
     Predicate<V> n = p.getChild(0);
-    if (n instanceof TimestampRangePredicate) {
-      return notTimestamp((TimestampRangePredicate<V>) n);
+    if (n instanceof TimestampRangePredicate<V> tr) {
+      return notTimestamp(tr);
     }
 
     // Lucene does not support negation, start with all and subtract.
@@ -181,8 +177,7 @@
   }
 
   private Query intRangeQuery(IndexPredicate<V> p) throws QueryParseException {
-    if (p instanceof IntegerRangePredicate) {
-      IntegerRangePredicate<V> r = (IntegerRangePredicate<V>) p;
+    if (p instanceof IntegerRangePredicate<V> r) {
       String name = r.getField().getName();
       int minimum = r.getMinimumValue();
       int maximum = r.getMaximumValue();
@@ -196,8 +191,7 @@
   }
 
   private Query timestampQuery(IndexPredicate<V> p) throws QueryParseException {
-    if (p instanceof TimestampRangePredicate) {
-      TimestampRangePredicate<V> r = (TimestampRangePredicate<V>) p;
+    if (p instanceof TimestampRangePredicate<V> r) {
       return LongPoint.newRangeQuery(
           r.getField().getName(),
           r.getMinTimestamp().toEpochMilli(),
@@ -215,13 +209,13 @@
   }
 
   private Query exactQuery(IndexPredicate<V> p) {
-    if (p instanceof RegexPredicate<?>) {
-      return regexQuery(p);
+    if (p instanceof RegexPredicate<V> r) {
+      return regexQuery(r);
     }
     return new TermQuery(new Term(p.getField().getName(), p.getValue()));
   }
 
-  private Query regexQuery(IndexPredicate<V> p) {
+  private Query regexQuery(RegexPredicate<V> p) {
     String re = p.getValue();
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index 56cb220..beb264b 100644
--- a/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -191,10 +191,10 @@
       while (true) {
         if (unwrapped == reader) {
           break;
-        } else if (unwrapped instanceof FilterDirectoryReader) {
-          unwrapped = ((FilterDirectoryReader) unwrapped).getDelegate();
-        } else if (unwrapped instanceof FilterLeafReader) {
-          unwrapped = ((FilterLeafReader) unwrapped).getDelegate();
+        } else if (unwrapped instanceof FilterDirectoryReader filterDirectoryReader) {
+          unwrapped = filterDirectoryReader.getDelegate();
+        } else if (unwrapped instanceof FilterLeafReader filterLeafReader) {
+          unwrapped = filterLeafReader.getDelegate();
         } else {
           break;
         }
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricInfo.java b/java/com/google/gerrit/metrics/dropwizard/MetricInfo.java
index 7abccf4..daa1ae8 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricInfo.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricInfo.java
@@ -77,8 +77,7 @@
   }
 
   private void init(Metric metric, ImmutableMap<String, String> atts) {
-    if (metric instanceof BucketedMetric) {
-      BucketedMetric m = (BucketedMetric) metric;
+    if (metric instanceof BucketedMetric m) {
       if (m.getTotal() != null) {
         init(m.getTotal(), atts);
       }
@@ -90,23 +89,19 @@
       }
       buckets = makeBuckets(fieldList, m.getCells(), atts);
 
-    } else if (metric instanceof Counter) {
-      Counter c = (Counter) metric;
+    } else if (metric instanceof Counter c) {
       count = c.getCount();
 
-    } else if (metric instanceof Gauge) {
-      Gauge<?> g = (Gauge<?>) metric;
+    } else if (metric instanceof Gauge<?> g) {
       value = g.getValue();
 
-    } else if (metric instanceof Meter) {
-      Meter m = (Meter) metric;
+    } else if (metric instanceof Meter m) {
       count = m.getCount();
       rate_1m = m.getOneMinuteRate();
       rate_5m = m.getFiveMinuteRate();
       rate_15m = m.getFifteenMinuteRate();
 
-    } else if (metric instanceof Timer) {
-      Timer m = (Timer) metric;
+    } else if (metric instanceof Timer m) {
       Snapshot s = m.getSnapshot();
       count = m.getCount();
       rate_1m = m.getOneMinuteRate();
@@ -125,8 +120,7 @@
       max = s.getMax() / div;
       std_dev = s.getStdDev() / div;
 
-    } else if (metric instanceof Histogram) {
-      Histogram m = (Histogram) metric;
+    } else if (metric instanceof Histogram m) {
       Snapshot s = m.getSnapshot();
       count = m.getCount();
 
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
index 84f2320..c62fb38 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
@@ -29,8 +29,8 @@
   @Nullable
   static OperatingSystemMXBeanInterface create() {
     OperatingSystemMXBean sys = ManagementFactory.getOperatingSystemMXBean();
-    if (sys instanceof UnixOperatingSystemMXBean) {
-      return new OperatingSystemMXBeanUnixNative((UnixOperatingSystemMXBean) sys);
+    if (sys instanceof UnixOperatingSystemMXBean unixOperatingSystemMXBean) {
+      return new OperatingSystemMXBeanUnixNative(unixOperatingSystemMXBean);
     }
 
     for (String name :
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 9c3e8b5..fc5a2c7 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.index.GerritIndexStatus;
-import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
@@ -116,7 +115,7 @@
 
   @Override
   protected boolean beforeInit(SiteInit init) throws Exception {
-    indexStatus = new GerritIndexStatus(IndexModule.indexDirectory(init.site));
+    indexStatus = new GerritIndexStatus(init.site);
     ErrorLogFile.errorOnlyConsole();
 
     if (!skipPlugins) {
diff --git a/java/com/google/gerrit/pgm/JythonShell.java b/java/com/google/gerrit/pgm/JythonShell.java
index dcd89dc..06d2a36 100644
--- a/java/com/google/gerrit/pgm/JythonShell.java
+++ b/java/com/google/gerrit/pgm/JythonShell.java
@@ -52,9 +52,7 @@
     // Jython needs to inspect actual .jar files (not just classloader)
     StringBuilder classPath = new StringBuilder();
     final ClassLoader cl = getClass().getClassLoader();
-    if (cl instanceof java.net.URLClassLoader) {
-      @SuppressWarnings("resource")
-      URLClassLoader ucl = (URLClassLoader) cl;
+    if (cl instanceof URLClassLoader ucl) {
       for (URL u : ucl.getURLs()) {
         if ("file".equals(u.getProtocol())) {
           if (classPath.length() > 0) {
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 6c4299d..faf9f16 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -70,6 +70,8 @@
 import org.eclipse.jetty.server.Handler;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.ProxyConnectionFactory;
+import org.eclipse.jetty.server.ProxyCustomizer;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.SecureRequestCustomizer;
 import org.eclipse.jetty.server.Server;
@@ -104,17 +106,24 @@
            * }
            * </code>
            *
-           * What we want to achieve here is to remember what it was the original proxy address before
-           * calling super.customize() and give the possibility to fetch it later down the chain.
+           * What we want to achieve here is to remember the original proxy address before calling
+           * super.customize() and give the possibility to fetch it later down the chain. When
+           * PROXY protocol is enabled, ProxyCustomizer has already exposed the underlying socket
+           * peer address through its request attribute.
            */
-          request.setAttribute(
-              RemoteUserUtil.PROXY_REMOTE_ADDRESS_ATTR,
-              ((InetSocketAddress) request.getConnectionMetaData().getRemoteSocketAddress())
-                  .getAddress()
-                  .getHostAddress());
+          String proxyRemoteAddress =
+              (String) request.getAttribute(ProxyCustomizer.REMOTE_ADDRESS_ATTRIBUTE_NAME);
+          if (proxyRemoteAddress == null) {
+            proxyRemoteAddress =
+                ((InetSocketAddress) request.getConnectionMetaData().getRemoteSocketAddress())
+                    .getAddress()
+                    .getHostAddress();
+          }
+          request.setAttribute(RemoteUserUtil.PROXY_REMOTE_ADDRESS_ATTR, proxyRemoteAddress);
           return super.customize(request, responseHeaders);
         }
       };
+  private static final ProxyCustomizer PROXY_CUSTOMIZER = new ProxyCustomizer();
 
   static class Lifecycle implements LifecycleListener {
     private final JettyServer server;
@@ -134,9 +143,8 @@
         server.httpd.start();
         if (rewrite) {
           Connector con = server.httpd.getConnectors()[0];
-          if (con instanceof ServerConnector) {
+          if (con instanceof ServerConnector serverCon) {
             @SuppressWarnings("resource")
-            ServerConnector serverCon = (ServerConnector) con;
             String host = serverCon.getHost();
             int port = serverCon.getLocalPort();
             String url = String.format("http://%s:%d", host, port);
@@ -336,11 +344,13 @@
     final int acceptors = cfg.getInt("httpd", "acceptorThreads", 0);
     final int selectors = cfg.getInt("httpd", "selectorThreads", 2);
     final AuthType authType = cfg.getEnum("auth", null, "type", AuthType.OPENID);
+    final boolean enableProxyProtocol = cfg.getBoolean("httpd", null, "enableProxyProtocol", false);
 
-    reverseProxy = isReverseProxied(listenUrls);
+    boolean allReverseProxy = true;
     final Connector[] connectors = new Connector[listenUrls.length];
     for (int idx = 0; idx < listenUrls.length; idx++) {
       final URI u = listenUrls[idx];
+      final String scheme = u.getScheme();
       final int defaultPort;
       final ServerConnector c;
       HttpConfiguration config = defaultConfig(requestHeaderSize);
@@ -372,10 +382,10 @@
                   UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING,
                   UriCompliance.Violation.SUSPICIOUS_PATH_CHARACTERS)));
 
-      if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && !"https".equals(u.getScheme())) {
+      if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && !"https".equals(scheme)) {
         throw new IllegalArgumentException(
             "Protocol '"
-                + u.getScheme()
+                + scheme
                 + "' "
                 + " not supported in httpd.listenurl '"
                 + u
@@ -384,11 +394,13 @@
                 + "'; only 'https' is supported");
       }
 
-      if ("http".equals(u.getScheme())) {
+      if ("http".equals(scheme)) {
+        allReverseProxy = false;
         defaultPort = 80;
-        c = newServerConnector(server, acceptors, selectors, config);
+        c = newServerConnector(server, acceptors, selectors, config, false);
 
-      } else if ("https".equals(u.getScheme())) {
+      } else if ("https".equals(scheme)) {
+        allReverseProxy = false;
         SslContextFactory.Server ssl = new SslContextFactory.Server();
         final Path keystore = getFile(cfg, "sslkeystore", "etc/keystore");
         String password = cfg.getString("httpd", null, "sslkeypassword");
@@ -424,41 +436,43 @@
                 new SslConnectionFactory(ssl, "http/1.1"),
                 new HttpConnectionFactory(config));
 
-      } else if ("proxy-http".equals(u.getScheme())) {
+      } else if ("proxy-http".equals(scheme) || "proxy-https".equals(scheme)) {
         defaultPort = 8080;
+        // Add the proxy customizer first so the forwarding customizer can record the underlying
+        // peer address used for authentication before applying X-Forwarded-For to the request.
+        if (enableProxyProtocol) {
+          config.addCustomizer(PROXY_CUSTOMIZER);
+        }
         config.addCustomizer(FORWARDED_REQUEST_CUSTOMIZER);
-        c = newServerConnector(server, acceptors, selectors, config);
+        if ("proxy-https".equals(scheme)) {
+          // For a proxy that terminates TLS, mark every request as HTTPS
+          // unconditionally. ForwardedRequestCustomizer alone only sets
+          // isSecure() when the proxy sends X-Forwarded-Proto=https or
+          // X-Proxied-Https=on; this wrapper covers proxies that don't.
+          // Jetty 12's HttpConfiguration.Customizer returns a (possibly
+          // wrapped) Request, so wrap the URI's scheme and override isSecure().
+          config.addCustomizer(
+              (request, responseHeaders) ->
+                  new Request.Wrapper(request) {
+                    @Override
+                    public HttpURI getHttpURI() {
+                      return HttpURI.build(super.getHttpURI())
+                          .scheme(HttpScheme.HTTPS.asString())
+                          .asImmutable();
+                    }
 
-      } else if ("proxy-https".equals(u.getScheme())) {
-        defaultPort = 8080;
-        config.addCustomizer(FORWARDED_REQUEST_CUSTOMIZER);
-        // For a proxy that terminates TLS, mark every request as HTTPS
-        // unconditionally. ForwardedRequestCustomizer alone only sets
-        // isSecure() when the proxy sends X-Forwarded-Proto=https or
-        // X-Proxied-Https=on; this wrapper covers proxies that don't.
-        // Jetty 12's HttpConfiguration.Customizer returns a (possibly
-        // wrapped) Request, so wrap the URI's scheme and override isSecure().
-        config.addCustomizer(
-            (request, responseHeaders) ->
-                new Request.Wrapper(request) {
-                  @Override
-                  public HttpURI getHttpURI() {
-                    return HttpURI.build(super.getHttpURI())
-                        .scheme(HttpScheme.HTTPS.asString())
-                        .asImmutable();
-                  }
-
-                  @Override
-                  public boolean isSecure() {
-                    return true;
-                  }
-                });
-        c = newServerConnector(server, acceptors, selectors, config);
+                    @Override
+                    public boolean isSecure() {
+                      return true;
+                    }
+                  });
+        }
+        c = newServerConnector(server, acceptors, selectors, config, enableProxyProtocol);
 
       } else {
         throw new IllegalArgumentException(
             "Protocol '"
-                + u.getScheme()
+                + scheme
                 + "' "
                 + " not supported in httpd.listenurl '"
                 + u
@@ -491,13 +505,30 @@
       c.setIdleTimeout(cfg.getTimeUnit("httpd", null, "idleTimeout", 30000L, MILLISECONDS));
       connectors[idx] = c;
     }
+    reverseProxy = allReverseProxy;
     return connectors;
   }
 
-  private static ServerConnector newServerConnector(
-      Server server, int acceptors, int selectors, HttpConfiguration config) {
-    return new ServerConnector(
-        server, null, null, null, acceptors, selectors, new HttpConnectionFactory(config));
+  @VisibleForTesting
+  static ServerConnector newServerConnector(
+      Server server,
+      int acceptors,
+      int selectors,
+      HttpConfiguration config,
+      boolean enableProxyProtocol) {
+    HttpConnectionFactory http = new HttpConnectionFactory(config);
+    if (enableProxyProtocol) {
+      return new ServerConnector(
+          server,
+          null,
+          null,
+          null,
+          acceptors,
+          selectors,
+          new ProxyConnectionFactory(http.getProtocol()),
+          http);
+    }
+    return new ServerConnector(server, null, null, null, acceptors, selectors, http);
   }
 
   private HttpConfiguration defaultConfig(int requestHeaderSize) {
@@ -521,15 +552,6 @@
     return config;
   }
 
-  static boolean isReverseProxied(URI[] listenUrls) {
-    for (URI u : listenUrls) {
-      if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   static URI[] listenURLs(Config cfg) {
     String[] urls = cfg.getStringList("httpd", null, "listenurl");
     if (urls.length == 0) {
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 3e51b98..3a7100d 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -274,8 +274,8 @@
       final Message first = ce.getErrorMessages().iterator().next();
       Throwable why = first.getCause();
 
-      if (why instanceof Die) {
-        throw (Die) why;
+      if (why instanceof Die die) {
+        throw die;
       }
 
       final StringBuilder buf = new StringBuilder(ce.getMessage());
diff --git a/java/com/google/gerrit/pgm/init/InitIndex.java b/java/com/google/gerrit/pgm/init/InitIndex.java
index 53a4aca..f6e5ede 100644
--- a/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.index.IndexType;
@@ -36,7 +35,6 @@
 /** Initialize the {@code index} configuration section. */
 @Singleton
 class InitIndex implements InitStep {
-  private static final String DIRECTORY = "directory";
   private final ConsoleUI ui;
   private final Section index;
   private final SitePaths site;
@@ -59,15 +57,10 @@
         new IndexType(
             index.select("Type", "type", IndexType.getDefault(), IndexType.getKnownTypes()));
 
-    if (Strings.isNullOrEmpty(index.get(DIRECTORY))) {
-      index.set(DIRECTORY, IndexModule.INDEX);
-    }
-    Path loc = IndexModule.indexDirectory(initFlags.cfg, site);
-    FileUtil.mkdirsOrDie(loc, "Cannot create index.directory");
-
     if ((site.isNew || isEmptySite()) && type.isLucene()) {
+      FileUtil.mkdirsOrDie(site.index_dir, "Cannot create index directory");
       for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
-        IndexUtils.setReady(loc, def.getName(), def.getLatest().getVersion(), true);
+        IndexUtils.setReady(site, def.getName(), def.getLatest().getVersion(), true);
       }
     } else {
       String message =
diff --git a/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index e68e203..4ec912c 100644
--- a/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -196,8 +196,8 @@
     if (classLoader.getParent() != null) {
       appendClasspath(cp, classLoader.getParent());
     }
-    if (classLoader instanceof URLClassLoader) {
-      for (URL url : ((URLClassLoader) classLoader).getURLs()) {
+    if (classLoader instanceof URLClassLoader urlClassLoader) {
+      for (URL url : urlClassLoader.getURLs()) {
         if ("file".equals(url.getProtocol())) {
           if (0 < cp.length()) {
             cp.append(File.pathSeparatorChar);
diff --git a/java/com/google/gerrit/server/DefaultRefLogIdentityProvider.java b/java/com/google/gerrit/server/DefaultRefLogIdentityProvider.java
index 10b4ec5..29cd53b 100644
--- a/java/com/google/gerrit/server/DefaultRefLogIdentityProvider.java
+++ b/java/com/google/gerrit/server/DefaultRefLogIdentityProvider.java
@@ -84,8 +84,7 @@
   private String guessHost(IdentifiedUser user) {
     String host = null;
     SocketAddress remotePeer = user.getRemotePeer();
-    if (remotePeer instanceof InetSocketAddress) {
-      InetSocketAddress sa = (InetSocketAddress) remotePeer;
+    if (remotePeer instanceof InetSocketAddress sa) {
       InetAddress in = sa.getAddress();
       host = in != null ? in.getHostAddress() : sa.getHostName();
     }
diff --git a/java/com/google/gerrit/server/DynamicOptions.java b/java/com/google/gerrit/server/DynamicOptions.java
index 03d7465..3bce2b1 100644
--- a/java/com/google/gerrit/server/DynamicOptions.java
+++ b/java/com/google/gerrit/server/DynamicOptions.java
@@ -251,8 +251,8 @@
     }
 
     String className = null;
-    if (dynamicBean instanceof ClassNameProvider) {
-      className = ((ClassNameProvider) dynamicBean).getClassName();
+    if (dynamicBean instanceof ClassNameProvider classNameProvider) {
+      className = classNameProvider.getClassName();
     } else if (!identity().equivalent(loader, beanCl)) { // in a different plugin?
       className = dynamicBean.getClass().getCanonicalName();
     }
@@ -261,10 +261,9 @@
       try {
         List<Module> modules = new ArrayList<>();
         Injector modulesInjector = injector;
-        if (dynamicBean instanceof ModulesClassNamesProvider) {
+        if (dynamicBean instanceof ModulesClassNamesProvider modulesClassNamesProvider) {
           modulesInjector = injector.createChildInjector();
-          for (String moduleName :
-              ((ModulesClassNamesProvider) dynamicBean).getModulesClassNames()) {
+          for (String moduleName : modulesClassNamesProvider.getModulesClassNames()) {
             Class<Module> mClass = (Class<Module>) loader.loadClass(moduleName);
             modules.add(modulesInjector.getInstance(mClass));
           }
@@ -307,8 +306,7 @@
   }
 
   public void setDynamicBeans() {
-    if (bean instanceof BeanReceiver) {
-      BeanReceiver receiver = (BeanReceiver) bean;
+    if (bean instanceof BeanReceiver receiver) {
       for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
         receiver.setDynamicBean(e.getKey(), e.getValue());
       }
@@ -326,8 +324,7 @@
   public void onBeanParseStart() {
     for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       DynamicBean instance = e.getValue();
-      if (instance instanceof BeanParseListener) {
-        BeanParseListener listener = (BeanParseListener) instance;
+      if (instance instanceof BeanParseListener listener) {
         listener.onBeanParseStart(e.getKey(), bean);
       }
     }
@@ -336,8 +333,7 @@
   public void onBeanParseEnd() {
     for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       DynamicBean instance = e.getValue();
-      if (instance instanceof BeanParseListener) {
-        BeanParseListener listener = (BeanParseListener) instance;
+      if (instance instanceof BeanParseListener listener) {
         listener.onBeanParseEnd(e.getKey(), bean);
       }
     }
diff --git a/java/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfig.java b/java/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfig.java
new file mode 100644
index 0000000..e7f52ce
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfig.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Configuration for auth token expiry notifications.
+ *
+ * <p>Defines when and how often to send notification emails to users before their authentication
+ * tokens expire.
+ */
+@Singleton
+public class AuthTokenExpiryNotificationConfig {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  static final String AUTH = "auth";
+  static final String TOKEN_EXPIRY = "token-expiry";
+  static final String NOTIFY = "notify";
+  static final String NOTIFY_DAYS_BEFORE = "notifyDaysBefore";
+
+  private static final List<Integer> DEFAULT_NOTIFICATION_DAYS = List.of(0, 7, 14, 21);
+
+  private final boolean enabled;
+  private final List<Integer> notificationDays;
+
+  @Inject
+  public AuthTokenExpiryNotificationConfig(@GerritServerConfig Config config)
+      throws ConfigInvalidException {
+    this.enabled = config.getBoolean(AUTH, TOKEN_EXPIRY, NOTIFY, true);
+    String[] configNotificationDays = config.getStringList(AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE);
+    if (configNotificationDays.length == 0) {
+      this.notificationDays = DEFAULT_NOTIFICATION_DAYS;
+    } else {
+      try {
+        this.notificationDays =
+            Arrays.stream(configNotificationDays)
+                .map(Integer::parseInt)
+                .filter(i -> i >= 0)
+                .toList();
+      } catch (NumberFormatException e) {
+        throw new ConfigInvalidException(
+            String.format(
+                "%s.%s.%s has to be a list of integers.", AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE),
+            e);
+      }
+      if (this.notificationDays.size() < configNotificationDays.length) {
+        logger.atWarning().log(
+            "Some invalid values were found in %s.%s.%s configuration"
+                + " and were ignored. Only non-negative integers are allowed.",
+            AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE);
+      }
+    }
+  }
+
+  /**
+   * Returns whether token expiry notifications are enabled.
+   *
+   * @return true if enabled, false otherwise
+   */
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  /**
+   * Returns a list of days before expiration on which to send notifications.
+   *
+   * <p>Only used when {@link #isEnabled()} returns true.
+   *
+   * @return list of days (default: [0,7,14,21])
+   */
+  public List<Integer> getNotificationDays() {
+    return notificationDays;
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java b/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java
index 986b7d0..06038d2 100644
--- a/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java
+++ b/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.server.mail.EmailFactories.AUTH_TOKEN_EXPIRED;
 import static com.google.gerrit.server.mail.EmailFactories.AUTH_TOKEN_WILL_EXPIRE;
 
+import autovalue.shaded.com.google.common.annotations.VisibleForTesting;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.ScheduleConfig;
@@ -28,20 +30,39 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * Scheduled task that sends notification emails to users when their authentication tokens are
+ * approaching expiration.
+ *
+ * <p>This notifier runs daily at midnight (00:00) and checks all user authentication tokens with
+ * expiration dates. Based on the configured notification schedule, it sends reminder emails at the
+ * configured days before tokens expire.
+ *
+ * <h3>Behavior</h3>
+ *
+ * <ul>
+ *   <li>Tokens that expired within the last 24 hours receive an expiration notification
+ *   <li>Tokens approaching expiration receive countdown notifications based on the schedule
+ *   <li>Only one email is sent per token per day (either expired or countdown notification)
+ *   <li>Tokens without expiration dates are skipped
+ * </ul>
+ */
 @Singleton
 public class AuthTokenExpiryNotifier implements Runnable {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final long FIRST_NOTIFICATION_BEFORE_EXPIRY = 7L; // 7 days
+
+  private static final Schedule DAILY_AT_MIDNIGHT =
+      ScheduleConfig.Schedule.createOrFail(Duration.ofDays(1).toMillis(), "00:00");
 
   private final Accounts accounts;
   private final AuthTokenAccessor tokenAccessor;
   private final EmailFactories emailFactories;
+  private final AuthTokenExpiryNotificationConfig config;
 
   public static Module module() {
     return new LifecycleModule() {
@@ -56,20 +77,16 @@
   static class Lifecycle implements LifecycleListener {
     private final WorkQueue queue;
     private final AuthTokenExpiryNotifier notifier;
-    private final Optional<Schedule> schedule;
 
     @Inject
     Lifecycle(WorkQueue queue, AuthTokenExpiryNotifier notifier) {
       this.queue = queue;
       this.notifier = notifier;
-      schedule = ScheduleConfig.Schedule.create(TimeUnit.DAYS.toMillis(1), "00:00");
     }
 
     @Override
     public void start() {
-      if (schedule.isPresent()) {
-        queue.scheduleAtFixedRate(notifier, schedule.get());
-      }
+      queue.scheduleAtFixedRate(notifier, DAILY_AT_MIDNIGHT);
     }
 
     @Override
@@ -80,43 +97,119 @@
 
   @Inject
   public AuthTokenExpiryNotifier(
-      Accounts accounts, AuthTokenAccessor tokenAccessor, EmailFactories emailFactories) {
+      Accounts accounts,
+      AuthTokenAccessor tokenAccessor,
+      EmailFactories emailFactories,
+      AuthTokenExpiryNotificationConfig config) {
     this.accounts = accounts;
     this.tokenAccessor = tokenAccessor;
     this.emailFactories = emailFactories;
+    this.config = config;
   }
 
+  /**
+   * Executes the daily token expiry notification check.
+   *
+   * <p>This method:
+   *
+   * <ol>
+   *   <li>Checks if notifications are enabled via configuration
+   *   <li>Iterates through all user accounts and their authentication tokens
+   *   <li>For each token with an expiration date:
+   *       <ul>
+   *         <li>If expired within last 24 hours: sends expiration notification
+   *         <li>Else, sends countdown notification if due
+   *       </ul>
+   * </ol>
+   *
+   * <p><b>Email Sending:</b> Emails are sent asynchronously via the {@code @SendEmailExecutor}
+   * thread pool. This prevents blocking the scheduled task and enables parallel email sending when
+   * the pool size is configured > 1. Configure via {@code sendemail.threadPoolSize} (default: 1).
+   *
+   * @throws RuntimeException if accounts cannot be read from NoteDB
+   */
   @Override
   public void run() {
-    Instant now = Instant.now();
+    if (!config.isEnabled()) {
+      logger.atFine().log("Auth token expiry notifications are disabled.");
+      return;
+    }
+
+    Instant checkTime = Instant.now();
+
     try {
       for (AccountState account : accounts.all()) {
-        for (AuthToken token : tokenAccessor.getTokens(account.account().id())) {
-          if (token.expirationDate().isEmpty()) {
-            continue;
-          }
-          Instant expirationDate = token.expirationDate().get();
-          if (expirationDate.isBefore(now.plus(FIRST_NOTIFICATION_BEFORE_EXPIRY, ChronoUnit.DAYS))
-              && expirationDate.isAfter(
-                  now.plus(FIRST_NOTIFICATION_BEFORE_EXPIRY - 1, ChronoUnit.DAYS))) {
-            logger.atInfo().log(
-                "Token %s for account %s is expiring soon.", token.id(), account.account().id());
-            try {
-              emailFactories
-                  .createOutgoingEmail(
-                      AUTH_TOKEN_WILL_EXPIRE,
-                      emailFactories.createAuthTokenWillExpireEmail(account.account(), token))
-                  .send();
-            } catch (EmailException e) {
-              logger.atSevere().withCause(e).log(
-                  "Failed to send token expiry notification email for token %s of account %s",
-                  token.id(), account.account().id());
+        try {
+          for (AuthToken token : tokenAccessor.getTokens(account.account().id())) {
+            if (token.expirationDate().isEmpty()) {
+              continue;
+            }
+            Instant expirationDate = token.expirationDate().get();
+
+            if (shouldNotifyExpired(checkTime, expirationDate)) {
+              notifyExpired(account.account(), token, expirationDate);
+            } else if (shouldBeNotified(checkTime, expirationDate)) {
+              notifyExpiring(account.account(), token, expirationDate);
             }
           }
+        } catch (IOException | ConfigInvalidException e) {
+          logger.atSevere().withCause(e).log(
+              "Failed to read tokens for account %s. Can't send token expiry notifications.",
+              account.account().id());
         }
       }
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException e) {
       throw new RuntimeException("Failed to read accounts from NoteDB", e);
     }
   }
+
+  private void notifyExpiring(Account account, AuthToken token, Instant expirationDate) {
+    logger.atInfo().log(
+        "Token %s for account %s is expiring on %s. Submitting notification.",
+        token.id(), account.id(), expirationDate);
+    emailFactories
+        .createOutgoingEmail(
+            AUTH_TOKEN_WILL_EXPIRE, emailFactories.createAuthTokenWillExpireEmail(account, token))
+        .sendAsync();
+  }
+
+  /**
+   * Determines whether a notification should be sent for a token expiring at the given date.
+   *
+   * @param checkInstant the instant to check expiration against
+   * @param expirationInstant the token expiration date
+   * @return true if a notification should be sent, false otherwise
+   */
+  @VisibleForTesting
+  boolean shouldBeNotified(Instant checkInstant, Instant expirationInstant) {
+    // Check if token already expired
+    if (expirationInstant.isBefore(checkInstant)) {
+      return false;
+    }
+
+    long daysUntilExpiration = ChronoUnit.DAYS.between(checkInstant, expirationInstant);
+    return config.getNotificationDays().contains((int) daysUntilExpiration);
+  }
+
+  private void notifyExpired(Account account, AuthToken token, Instant expirationDate) {
+    logger.atInfo().log(
+        "Token %s for account %s has expired on %s. Submitting expiration notification.",
+        token.id(), account.id(), expirationDate);
+    emailFactories
+        .createOutgoingEmail(
+            AUTH_TOKEN_EXPIRED, emailFactories.createAuthTokenExpiredEmail(account, token))
+        .sendAsync();
+  }
+
+  /**
+   * Determines if a token has expired during the last day and should receive an expiration
+   * notification.
+   *
+   * @param expirationDate the token's expiration date
+   * @return true if an expiration notification should be sent, false otherwise
+   */
+  static boolean shouldNotifyExpired(Instant checkTime, Instant expirationDate) {
+    return !expirationDate.isAfter(checkTime)
+        && !expirationDate.isBefore(checkTime.minus(1, ChronoUnit.DAYS));
+  }
 }
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 59f56e8..00caa95 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -160,8 +160,8 @@
     }
 
     // Keep this logic in sync with DefaultRefFilter#isGroupOwner(...).
-    if (group instanceof GroupDescription.Internal) {
-      AccountGroup.UUID ownerUUID = ((GroupDescription.Internal) group).getOwnerGroupUUID();
+    if (group instanceof GroupDescription.Internal internalGroup) {
+      AccountGroup.UUID ownerUUID = internalGroup.getOwnerGroupUUID();
       if (getUser().getEffectiveGroups().contains(ownerUUID)) {
         logger.atFine().log(
             "user %s is owner of group %s", user.getLoggableName(), group.getGroupUUID().get());
@@ -228,8 +228,8 @@
     if (user.isInternalUser()) {
       return true;
     }
-    if (group instanceof GroupDescription.Internal) {
-      return ((GroupDescription.Internal) group).isVisibleToAll() || isOwner();
+    if (group instanceof GroupDescription.Internal internalGroup) {
+      return internalGroup.isVisibleToAll() || isOwner();
     }
     return canAdministrateServer();
   }
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 45214a6..0d92024 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -136,7 +136,7 @@
     if (options.equals(ID_ONLY)) {
       return;
     }
-    if (in instanceof Collection && ((Collection<?>) in).isEmpty()) {
+    if (in instanceof Collection<?> collection && collection.isEmpty()) {
       return;
     }
 
@@ -201,7 +201,7 @@
 
   @Override
   public void fillAccountAttributeInfo(Iterable<? extends AccountAttribute> in) {
-    if (in instanceof Collection && ((Collection<?>) in).isEmpty()) {
+    if (in instanceof Collection<?> collection && collection.isEmpty()) {
       return;
     }
     Set<Account.Id> ids;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
index f2a5885..bbccdab 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
@@ -30,13 +30,13 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.AllUsersRepository;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import java.io.IOException;
@@ -69,8 +69,7 @@
 
   private final ExternalIdReader externalIdReader;
   private final Cache<ObjectId, AllExternalIds> externalIdCache;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final AllUsersName allUsersName;
+  private final Provider<Repository> allUsersRepositoryProvider;
   private final Counter1<Boolean> reloadCounter;
   private final Timer0 reloadDifferential;
   private final boolean isPersistentCache;
@@ -78,8 +77,7 @@
 
   @Inject
   ExternalIdCacheLoader(
-      GitRepositoryManager gitRepositoryManager,
-      AllUsersName allUsersName,
+      @AllUsersRepository Provider<Repository> allUsersRepositoryProvider,
       ExternalIdReader externalIdReader,
       @Named(ExternalIdCacheImpl.CACHE_NAME) Cache<ObjectId, AllExternalIds> externalIdCache,
       MetricMaker metricMaker,
@@ -87,8 +85,7 @@
       ExternalIdFactoryNoteDbImpl externalIdFactory) {
     this.externalIdReader = externalIdReader;
     this.externalIdCache = externalIdCache;
-    this.gitRepositoryManager = gitRepositoryManager;
-    this.allUsersName = allUsersName;
+    this.allUsersRepositoryProvider = allUsersRepositoryProvider;
     this.reloadCounter =
         metricMaker.newCounter(
             "notedb/external_id_cache_load_count",
@@ -130,8 +127,8 @@
     // Once we know what files changed, we apply additions and removals to the previously cached
     // state.
 
-    try (Repository repo = gitRepositoryManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(repo)) {
+    Repository repo = allUsersRepositoryProvider.get();
+    try (RevWalk rw = new RevWalk(repo)) {
       long start = System.nanoTime();
       Ref extIdRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
       if (extIdRef == null) {
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
index 1d58bd2..1ff957d 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
@@ -28,8 +28,9 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.AllUsersRepository;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.time.Duration;
@@ -72,8 +73,8 @@
     return NoteMap.newEmptyMap();
   }
 
-  private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
+  private final Provider<Repository> allUsersRepositoryProvider;
   private boolean failOnLoad = false;
   private final Timer0 readAllLatency;
   private final Timer0 readSingleLatency;
@@ -85,13 +86,13 @@
   @VisibleForTesting
   @Inject
   public ExternalIdReader(
-      GitRepositoryManager repoManager,
+      @AllUsersRepository Provider<Repository> allUsersRepositoryProvider,
       AllUsersName allUsersName,
       MetricMaker metricMaker,
       ExternalIdFactoryNoteDbImpl externalIdFactory,
       AuthConfig authConfig) {
-    this.repoManager = repoManager;
     this.allUsersName = allUsersName;
+    this.allUsersRepositoryProvider = allUsersRepositoryProvider;
     this.readAllLatency =
         metricMaker.newTimer(
             "notedb/read_all_external_ids_latency",
@@ -113,7 +114,7 @@
                 () -> {
                   try {
                     logger.atFine().log("Refreshing external-ids revision from All-Users repo");
-                    return readRevision(repoManager, allUsersName);
+                    return readRevision(allUsersRepositoryProvider.get());
                   } catch (IOException e) {
                     throw new IllegalStateException(
                         "Couldn't refresh external-ids from All-Users repo", e);
@@ -137,14 +138,7 @@
   public ObjectId readRevision() throws IOException {
     return externalIdsRefExpirySecs > 0
         ? allUsersSupplier.get()
-        : readRevision(repoManager, allUsersName);
-  }
-
-  private static ObjectId readRevision(GitRepositoryManager repoManager, AllUsersName allUsersName)
-      throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      return readRevision(repo);
-    }
+        : readRevision(allUsersRepositoryProvider.get());
   }
 
   public static ObjectId readRevision(Repository repo) throws IOException {
@@ -156,11 +150,10 @@
   ImmutableSet<ExternalId> all() throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
-    try (Timer0.Context ctx = readAllLatency.start();
-        Repository repo = repoManager.openRepository(allUsersName)) {
+    try (Timer0.Context ctx = readAllLatency.start()) {
       return ExternalIdNotes.loadReadOnly(
               allUsersName,
-              repo,
+              allUsersRepositoryProvider.get(),
               null,
               externalIdFactory,
               authConfig.isUserNameCaseInsensitiveMigrationMode())
@@ -182,11 +175,10 @@
       throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
-    try (Timer0.Context ctx = readAllLatency.start();
-        Repository repo = repoManager.openRepository(allUsersName)) {
+    try (Timer0.Context ctx = readAllLatency.start()) {
       return ExternalIdNotes.loadReadOnly(
               allUsersName,
-              repo,
+              allUsersRepositoryProvider.get(),
               rev,
               externalIdFactory,
               authConfig.isUserNameCaseInsensitiveMigrationMode())
@@ -198,11 +190,10 @@
   Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
-    try (Timer0.Context ctx = readSingleLatency.start();
-        Repository repo = repoManager.openRepository(allUsersName)) {
+    try (Timer0.Context ctx = readSingleLatency.start()) {
       return ExternalIdNotes.loadReadOnly(
               allUsersName,
-              repo,
+              allUsersRepositoryProvider.get(),
               null,
               externalIdFactory,
               authConfig.isUserNameCaseInsensitiveMigrationMode())
@@ -215,11 +206,10 @@
       throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
-    try (Timer0.Context ctx = readSingleLatency.start();
-        Repository repo = repoManager.openRepository(allUsersName)) {
+    try (Timer0.Context ctx = readSingleLatency.start()) {
       return ExternalIdNotes.loadReadOnly(
               allUsersName,
-              repo,
+              allUsersRepositoryProvider.get(),
               rev,
               externalIdFactory,
               authConfig.isUserNameCaseInsensitiveMigrationMode())
diff --git a/java/com/google/gerrit/server/auth/AuthUser.java b/java/com/google/gerrit/server/auth/AuthUser.java
index 9e1c5ec..b71fa21 100644
--- a/java/com/google/gerrit/server/auth/AuthUser.java
+++ b/java/com/google/gerrit/server/auth/AuthUser.java
@@ -70,8 +70,8 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof AuthUser) {
-      return getUUID().equals(((AuthUser) obj).getUUID());
+    if (obj instanceof AuthUser authUser) {
+      return getUUID().equals(authUser.getUUID());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/cache/CacheInfoFactory.java b/java/com/google/gerrit/server/cache/CacheInfoFactory.java
index 48122db..3a2de36 100644
--- a/java/com/google/gerrit/server/cache/CacheInfoFactory.java
+++ b/java/com/google/gerrit/server/cache/CacheInfoFactory.java
@@ -42,10 +42,10 @@
     cacheInfo.hitRatio = new HitRatioInfo();
     cacheInfo.hitRatio.setMem(stat.hitCount(), stat.requestCount());
 
-    if (cache instanceof PersistentCache) {
+    if (cache instanceof PersistentCache persistentCache) {
       cacheInfo.type = CacheType.DISK;
       if (includeDiskStats) {
-        PersistentCache.DiskStats diskStats = ((PersistentCache) cache).diskStats();
+        PersistentCache.DiskStats diskStats = persistentCache.diskStats();
         cacheInfo.entries.setDisk(diskStats.size());
         cacheInfo.entries.setSpace(diskStats.space());
         cacheInfo.hitRatio.setDisk(diskStats.hitCount(), diskStats.requestCount());
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 83878fe..a12a2f6 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ScheduleConfig;
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
+import com.google.gerrit.server.util.H2Url;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -229,7 +230,7 @@
   private <V, K> SqlStore<K, V> newSqlStore(PersistentCacheDef<K, V> def, long maxSize) {
     String cacheName = def.name() + "-v" + COMPATIBILITY_VERSION;
     StringBuilder url = new StringBuilder();
-    url.append("jdbc:h2:file:")
+    url.append(H2Url.ASYNC_PREFIX)
         .append(cacheDir.resolve(cacheName).toAbsolutePath().toString().replace(";", "\\;"));
     if (h2CacheSize >= 0) {
       url.append(";CACHE_SIZE=");
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 7097eb8..0df5380 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -109,8 +109,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof EntryKey) {
-        EntryKey k = (EntryKey) o;
+      if (o instanceof EntryKey k) {
         return commit.equals(k.commit)
             && into.equals(k.into)
             && submitType == k.submitType
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 4716a8c..a5d75f6 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -498,23 +498,22 @@
         firstNonNull(Strings.emptyToNull(mergeStrategy), mergeUtil.mergeStrategyName());
 
     Merger merger = MergeUtil.newMerger(ctx.getInserter(), ctx.getRepoView().getConfig(), strategy);
-    if (merger instanceof ThreeWayMerger) {
-      ((ThreeWayMerger) merger).setBase(parentCommit);
+    if (merger instanceof ThreeWayMerger threeWayMerger) {
+      threeWayMerger.setBase(parentCommit);
     }
 
     DirCache dc = DirCache.newInCore();
-    if (merger instanceof ResolveMerger) {
+    if (merger instanceof ResolveMerger resolveMerger) {
       if (useGitattributesForMerge) {
         // We need to set the attributes provider before attempting the merge in order to read and
         // honor gitattributes merge settings correctly
-        ((ResolveMerger) merger)
-            .setAttributesNodeProvider(ctx.getRepoView().getAttributesNodeProvider());
+        resolveMerger.setAttributesNodeProvider(ctx.getRepoView().getAttributesNodeProvider());
       }
       if (allowConflicts) {
         // The DirCache must be set on ResolveMerger before calling
         // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get
         // populated.
-        ((ResolveMerger) merger).setDirCache(dc);
+        resolveMerger.setDirCache(dc);
       }
     }
 
@@ -531,9 +530,9 @@
     } else {
       List<String> conflicts = ImmutableList.of();
       Map<String, ResolveMerger.MergeFailureReason> failed = ImmutableMap.of();
-      if (merger instanceof ResolveMerger) {
-        conflicts = ((ResolveMerger) merger).getUnmergedPaths();
-        failed = ((ResolveMerger) merger).getFailingPaths();
+      if (merger instanceof ResolveMerger resolveMerger) {
+        conflicts = resolveMerger.getUnmergedPaths();
+        failed = resolveMerger.getFailingPaths();
       }
 
       if (merger.getResultTreeId() != null) {
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index 1808ff5..6d71d86 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -309,8 +309,8 @@
     boolean exactMatchFound = false;
     try {
       if (ReviewerState.REMOVED.equals(input.state)
-          || (input instanceof InternalReviewerInput
-              && ((InternalReviewerInput) input).skipVisibilityCheck)) {
+          || (input instanceof InternalReviewerInput internalInput
+              && internalInput.skipVisibilityCheck)) {
         reviewerUser =
             accountResolver.resolveIncludeInactiveIgnoreVisibility(input.reviewer).asUniqueUser();
       } else {
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index cc9f8e0..199d8eb 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
@@ -23,6 +24,9 @@
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 
 /** Important paths within a {@link SitePath}. */
 @Singleton
@@ -44,6 +48,7 @@
   public final Path mail_dir;
   public final Path hooks_dir;
   public final Path static_dir;
+  public final Path index_dir;
 
   public final Path gerrit_sh;
   public final Path gerrit_service;
@@ -90,6 +95,7 @@
     mail_dir = etc_dir.resolve("mail");
     hooks_dir = p.resolve("hooks");
     static_dir = p.resolve("static");
+    index_dir = resolveIndexDir(p, etc_dir.resolve("gerrit.config"));
 
     gerrit_sh = bin_dir.resolve("gerrit.sh");
     gerrit_service = bin_dir.resolve("gerrit.service");
@@ -126,6 +132,27 @@
     this.isNew = isNew;
   }
 
+  private static Path resolveIndexDir(Path sitePath, Path gerritConfig) throws IOException {
+    FileBasedConfig cfg = new FileBasedConfig(gerritConfig.toFile(), FS.DETECTED);
+    if (cfg.getFile().exists()) {
+      try {
+        cfg.load();
+      } catch (ConfigInvalidException e) {
+        throw new IOException("Invalid config file " + gerritConfig, e);
+      }
+    }
+    String configured = cfg.getString("index", null, "directory");
+    if (!Strings.isNullOrEmpty(configured)) {
+      Path loc = sitePath.resolve(configured).normalize();
+      try {
+        return loc.toRealPath();
+      } catch (IOException e) {
+        return loc.toAbsolutePath();
+      }
+    }
+    return sitePath.resolve("index");
+  }
+
   /**
    * Resolve an absolute or relative path.
    *
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index dc52f87..6733097 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -128,8 +128,7 @@
 
   @Nullable
   private String findTitle(Node root) {
-    if (root instanceof Heading) {
-      Heading h = (Heading) root;
+    if (root instanceof Heading h) {
       if (h.getLevel() == 1 && h.hasChildren()) {
         TextCollectingVisitor collectingVisitor = new TextCollectingVisitor();
         return collectingVisitor.collectAndGetText(h);
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java b/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
index 04d2e5b..f408547 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatterHeader.java
@@ -66,7 +66,7 @@
     void render(final AnchorLink node, final NodeRendererContext context) {
       Node parent = node.getParent();
 
-      if (parent instanceof Heading && ((Heading) parent).getLevel() == 1) {
+      if (parent instanceof Heading heading && heading.getLevel() == 1) {
         // render without anchor link
         context.renderChildren(node);
       } else {
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index ad271b6..7a843b6 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -634,11 +634,11 @@
       merger.setBase(basePatchSetCommitId);
 
       DirCache dc = DirCache.newInCore();
-      if (allowConflicts && merger instanceof ResolveMerger) {
+      if (allowConflicts && merger instanceof ResolveMerger resolveMerger) {
         // The DirCache must be set on ResolveMerger before calling
         // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get
         // populated.
-        ((ResolveMerger) merger).setDirCache(dc);
+        resolveMerger.setDirCache(dc);
       }
 
       boolean successful = merger.merge(basePatchSetCommit, editCommitId);
@@ -650,8 +650,8 @@
         filesWithGitConflicts = null;
       } else {
         List<String> conflicts = ImmutableList.of();
-        if (merger instanceof ResolveMerger) {
-          conflicts = ((ResolveMerger) merger).getUnmergedPaths();
+        if (merger instanceof ResolveMerger resolveMerger) {
+          conflicts = resolveMerger.getUnmergedPaths();
         }
 
         if (!allowConflicts || !(merger instanceof ResolveMerger)) {
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index f4e1598..be41045 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -64,7 +64,19 @@
   public List<DirCacheEditor.PathEdit> getPathEdits(
       Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents) {
     DirCacheEditor.PathEdit changeContentEdit =
-        new ChangeContent(filePath, newContent, repository, newGitFileMode);
+        new ChangeContent(
+            filePath, newContent, repository, /* objectInserter= */ null, newGitFileMode);
+    return Collections.singletonList(changeContentEdit);
+  }
+
+  @Override
+  public List<DirCacheEditor.PathEdit> getPathEdits(
+      Repository repository,
+      ObjectInserter objectInserter,
+      ObjectId treeId,
+      ImmutableList<? extends ObjectId> parents) {
+    DirCacheEditor.PathEdit changeContentEdit =
+        new ChangeContent(filePath, newContent, repository, objectInserter, newGitFileMode);
     return Collections.singletonList(changeContentEdit);
   }
 
@@ -82,17 +94,20 @@
     private final String filePath;
     private final RawInput newContent;
     private final Repository repository;
+    private final @Nullable ObjectInserter objectInserter;
     private final Integer newGitFileMode;
 
     ChangeContent(
         String filePath,
         RawInput newContent,
         Repository repository,
+        @Nullable ObjectInserter objectInserter,
         @Nullable Integer newGitFileMode) {
       super(filePath);
       this.filePath = filePath;
       this.newContent = newContent;
       this.repository = repository;
+      this.objectInserter = objectInserter;
       this.newGitFileMode = newGitFileMode;
     }
 
@@ -141,21 +156,24 @@
     }
 
     private ObjectId createNewBlobAndGetItsId() throws IOException {
-      try (ObjectInserter objectInserter = repository.newObjectInserter()) {
-        ObjectId blobObjectId = createNewBlobAndGetItsId(objectInserter);
-        objectInserter.flush();
+      if (objectInserter != null) {
+        return createNewBlobAndGetItsId(objectInserter);
+      }
+      try (ObjectInserter inserter = repository.newObjectInserter()) {
+        ObjectId blobObjectId = createNewBlobAndGetItsId(inserter);
+        inserter.flush();
         return blobObjectId;
       }
     }
 
-    private ObjectId createNewBlobAndGetItsId(ObjectInserter objectInserter) throws IOException {
+    private ObjectId createNewBlobAndGetItsId(ObjectInserter inserter) throws IOException {
       long contentLength = newContent.getContentLength();
       if (contentLength < 0) {
-        return objectInserter.insert(OBJ_BLOB, getNewContentBytes());
+        return inserter.insert(OBJ_BLOB, getNewContentBytes());
       }
       try {
         InputStream contentInputStream = newContent.getInputStream();
-        return objectInserter.insert(OBJ_BLOB, contentLength, contentInputStream);
+        return inserter.insert(OBJ_BLOB, contentLength, contentInputStream);
       } catch (EOFException e) {
         if (e.getMessage().equals(JGitText.get().shortReadOfBlock)) {
           throw new BadContentLengthException(
diff --git a/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index ae80fe4..314b9dd 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -113,8 +113,15 @@
    */
   public ObjectId createNewTreeAndGetId(Repository repository) throws IOException {
     ensureTreeModificationsDoNotTouchSameFiles();
-    DirCache newTree = createNewTree(repository);
-    return writeAndGetId(repository, newTree);
+    ObjectInserter oi = objectInserter.orElseGet(() -> repository.newObjectInserter());
+    try {
+      DirCache newTree = createNewTree(repository, oi);
+      return writeAndGetId(oi, newTree);
+    } finally {
+      if (objectInserter.isEmpty()) {
+        oi.close();
+      }
+    }
   }
 
   private void ensureTreeModificationsDoNotTouchSameFiles() {
@@ -139,9 +146,9 @@
     }
   }
 
-  private DirCache createNewTree(Repository repository) throws IOException {
+  private DirCache createNewTree(Repository repository, ObjectInserter oi) throws IOException {
     DirCache newTree = readBaseTree(repository);
-    List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository);
+    List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository, oi);
     applyPathEdits(newTree, pathEdits);
     return newTree;
   }
@@ -166,26 +173,21 @@
     }
   }
 
-  private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) throws IOException {
+  private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, ObjectInserter oi)
+      throws IOException {
     List<DirCacheEditor.PathEdit> pathEdits = new ArrayList<>();
     for (TreeModification treeModification : treeModifications) {
       pathEdits.addAll(
-          treeModification.getPathEdits(repository, baseTreeId, ImmutableList.copyOf(baseParents)));
+          treeModification.getPathEdits(
+              repository, oi, baseTreeId, ImmutableList.copyOf(baseParents)));
     }
     return pathEdits;
   }
 
-  private ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException {
-    ObjectInserter oi = objectInserter.orElseGet(() -> repository.newObjectInserter());
-    try {
-      ObjectId treeId = tree.writeTree(oi);
-      oi.flush();
-      return treeId;
-    } finally {
-      if (objectInserter.isEmpty()) {
-        oi.close();
-      }
-    }
+  private static ObjectId writeAndGetId(ObjectInserter oi, DirCache tree) throws IOException {
+    ObjectId treeId = tree.writeTree(oi);
+    oi.flush();
+    return treeId;
   }
 
   private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) {
diff --git a/java/com/google/gerrit/server/edit/tree/TreeModification.java b/java/com/google/gerrit/server/edit/tree/TreeModification.java
index ba301fc..e376361 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeModification.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -20,6 +20,7 @@
 import java.util.List;
 import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 
 /** A specific modification of a Git tree. */
@@ -42,6 +43,33 @@
       throws IOException;
 
   /**
+   * Returns a list of {@code PathEdit}s which are necessary in order to achieve the desired
+   * modification of the Git tree. The order of the {@code PathEdit}s can be crucial and hence
+   * shouldn't be changed.
+   *
+   * <p>The default implementation delegates to {@link #getPathEdits(Repository, ObjectId,
+   * ImmutableList)}. Implementations that create new Git objects should override this method to
+   * insert them into the provided {@link ObjectInserter} rather than creating and flushing a
+   * separate inserter.
+   *
+   * @param repository the affected Git repository
+   * @param objectInserter the {@code ObjectInserter} to use for inserting new Git objects
+   * @param treeId tree to which the modification is applied. A value of {@code ObjectId.zero()}
+   *     indicates an empty tree.
+   * @param parents parent commits of the commit to whose tree this modification is applied
+   * @return an ordered list of necessary {@code PathEdit}s
+   * @throws IOException if problems arise when accessing the repository
+   */
+  default List<DirCacheEditor.PathEdit> getPathEdits(
+      Repository repository,
+      ObjectInserter objectInserter,
+      ObjectId treeId,
+      ImmutableList<? extends ObjectId> parents)
+      throws IOException {
+    return getPathEdits(repository, treeId, parents);
+  }
+
+  /**
    * Indicates all file paths affected by this {@code TreeModification}. If the modification refers
    * to several file paths (e.g. renaming a file), all of them must be returned.
    *
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 2edc642..73543cd 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -208,8 +208,7 @@
   }
 
   protected boolean isVisibleTo(Event event, CurrentUser user) throws PermissionBackendException {
-    if (event instanceof RefEvent) {
-      RefEvent refEvent = (RefEvent) event;
+    if (event instanceof RefEvent refEvent) {
       String ref = refEvent.getRefName();
       if (PatchSet.isChangeRef(ref)) {
         Change.Id cid = PatchSet.Id.fromRef(ref).changeId();
@@ -222,8 +221,8 @@
         }
       }
       return isVisibleTo(refEvent.getBranchNameKey(), user);
-    } else if (event instanceof ProjectEvent) {
-      return isVisibleTo(((ProjectEvent) event).getProjectNameKey(), user);
+    } else if (event instanceof ProjectEvent projectEvent) {
+      return isVisibleTo(projectEvent.getProjectNameKey(), user);
     }
     return true;
   }
diff --git a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index 5d2b7a5..516af9a 100644
--- a/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.server.data.AccountAttribute;
 
 public class ReviewerAddedEvent extends PatchSetEvent {
-  static final String TYPE = "reviewer-added";
+  public static final String TYPE = "reviewer-added";
   public Supplier<AccountAttribute> reviewer;
   public Supplier<AccountAttribute> adder;
 
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 07ec120..0b36b25 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -356,7 +356,7 @@
         event.patchSet = patchSetAttrs;
         event.adder = adderAccountAttrs;
         event.reviewer = accountAttributeSupplier(reviewer);
-        dispatcher.run(d -> d.postEvent(event));
+        dispatcher.run(d -> d.postEvent(change, event));
       }
     } catch (StorageException e) {
       logger.atSevere().withCause(e).log("Failed to dispatch event");
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 30c0ffe..4af476e 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -19,10 +19,6 @@
 /** Constants for Gerrit {@link ExperimentFeatures} */
 public class ExperimentFeaturesConstants {
 
-  /** Features that are known experiments and can be referenced in the code. */
-  public static String GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION =
-      "GerritBackendFeature__attach_nonce_to_documentation";
-
   /** Features, enabled by default in the current release. */
   public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES = ImmutableSet.of();
 
@@ -45,4 +41,13 @@
   /** Whether we restrict the creation of branch permissions. */
   public static final String GERRIT_BACKEND_FEATURE_RESTRICT_BRANCH_PERMISSIONS =
       "GerritBackendFeature__restrict_branch_permissions";
+
+  /**
+   * Whether registered users may use AI Review even when no explicit {@code aiReview} rule is
+   * configured. Lets sites enable the feature before adding the permission to {@code
+   * project.config}. Like all experiments, it is read at startup, so toggling it requires a server
+   * restart.
+   */
+  public static final String ALLOW_AI_REVIEW_FOR_REGISTERED_USERS =
+      "GerritBackendFeature__allow_ai_review_for_registered_users";
 }
diff --git a/java/com/google/gerrit/server/git/GitModule.java b/java/com/google/gerrit/server/git/GitModule.java
index 0aa0239..c84a281 100644
--- a/java/com/google/gerrit/server/git/GitModule.java
+++ b/java/com/google/gerrit/server/git/GitModule.java
@@ -24,7 +24,6 @@
 public class GitModule extends LifecycleModule {
   @Override
   protected void configure() {
-    install(new AllUsersRepositoryProvider.Module());
     factory(MetaDataUpdate.InternalFactory.class);
     bind(MetaDataUpdate.Server.class);
     DynamicSet.bind(binder(), PostUploadHook.class).to(UploadPackMetricsHook.class);
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 9a2d3e9..c194443 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -242,10 +242,10 @@
     m.setBase(baseCommit);
 
     DirCache dc = DirCache.newInCore();
-    if (allowConflicts && m instanceof ResolveMerger) {
+    if (allowConflicts && m instanceof ResolveMerger resolveMerger) {
       // The DirCache must be set on ResolveMerger before calling
       // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
-      ((ResolveMerger) m).setDirCache(dc);
+      resolveMerger.setDirCache(dc);
     }
 
     ObjectId tree;
@@ -488,10 +488,10 @@
     Merger m = newMerger(inserter, repoConfig, mergeStrategy);
 
     DirCache dc = DirCache.newInCore();
-    if (allowConflicts && m instanceof ResolveMerger) {
+    if (allowConflicts && m instanceof ResolveMerger resolveMerger) {
       // The DirCache must be set on ResolveMerger before calling
       // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
-      ((ResolveMerger) m).setDirCache(dc);
+      resolveMerger.setDirCache(dc);
     }
 
     ObjectId tree;
@@ -512,9 +512,9 @@
     } else {
       List<String> conflicts = ImmutableList.of();
       Map<String, ResolveMerger.MergeFailureReason> failed = ImmutableMap.of();
-      if (m instanceof ResolveMerger) {
-        conflicts = ((ResolveMerger) m).getUnmergedPaths();
-        failed = ((ResolveMerger) m).getFailingPaths();
+      if (m instanceof ResolveMerger resolveMerger) {
+        conflicts = resolveMerger.getUnmergedPaths();
+        failed = resolveMerger.getFailingPaths();
       }
 
       if (m.getResultTreeId() != null) {
@@ -1092,8 +1092,8 @@
         m instanceof ThreeWayMerger,
         "merge strategy %s does not support three-way merging",
         strategyName);
-    if (m instanceof ResolveMerger && useGitattributesForMerge) {
-      ((ResolveMerger) m).setAttributesNodeProvider(attributesNodeProvider);
+    if (m instanceof ResolveMerger resolveMerger && useGitattributesForMerge) {
+      resolveMerger.setAttributesNodeProvider(attributesNodeProvider);
     }
     return (ThreeWayMerger) m;
   }
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index fb34753..edbb2d9 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -94,8 +94,8 @@
     try {
       result = forProject.filter(ImmutableList.of(ref), getDelegate(), RefFilterOptions.defaults());
     } catch (PermissionBackendException e) {
-      if (e.getCause() instanceof IOException) {
-        throw (IOException) e.getCause();
+      if (e.getCause() instanceof IOException ioe) {
+        throw ioe;
       }
       throw new IOException(e);
     }
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index ff3821a..f22c7f6 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -644,8 +644,8 @@
       // Periodic Tasks may get rescheduled if the previous run has yet to fully complete (and thus
       // passed to decorateTask() more than once), and there is no need to redecorate them if they
       // are already decorated.
-      if (runnable instanceof LoggingContextAwareRunnable) {
-        Runnable unwrappedTask = ((LoggingContextAwareRunnable) runnable).unwrap();
+      if (runnable instanceof LoggingContextAwareRunnable loggingContextAwareRunnable) {
+        Runnable unwrappedTask = loggingContextAwareRunnable.unwrap();
         if (unwrappedTask instanceof Task<?>) {
           return r;
         }
@@ -657,12 +657,12 @@
 
         Task<V> task;
 
-        if (runnable instanceof LoggingContextAwareRunnable) {
-          runnable = ((LoggingContextAwareRunnable) runnable).unwrap();
+        if (runnable instanceof LoggingContextAwareRunnable loggingContextAwareRunnable) {
+          runnable = loggingContextAwareRunnable.unwrap();
         }
 
-        if (runnable instanceof ProjectRunnable) {
-          task = new ProjectTask<>((ProjectRunnable) runnable, r, nanosPeriod, this, id);
+        if (runnable instanceof ProjectRunnable projectRunnable) {
+          task = new ProjectTask<>(projectRunnable, r, nanosPeriod, this, id);
         } else {
           task = new Task<>(runnable, r, nanosPeriod, this, id);
         }
@@ -725,8 +725,7 @@
           extension -> {
             if (isReady.isTrue()) {
               TaskListener listener = extension.get();
-              if (listener instanceof TaskParker) {
-                TaskParker parker = (TaskParker) listener;
+              if (listener instanceof TaskParker parker) {
                 if (parker.isReadyToStart(task)) {
                   readyParkers.add(parker);
                 } else {
@@ -914,23 +913,23 @@
         // as running and allow it to clean up. This ensures we do
         // not invoke cancel twice.
         //
-        if (runnable instanceof CancelableRunnable) {
+        if (runnable instanceof CancelableRunnable cancelableRunnable) {
           if (runningState.compareAndSet(null, State.RUNNING)) {
             isSetRunningDuringCancellation = true;
-            ((CancelableRunnable) runnable).cancel();
+            cancelableRunnable.cancel();
           } else if (isWaitingToStart()) {
-            ((CancelableRunnable) runnable).cancel();
-          } else if (runnable instanceof CanceledWhileRunning) {
-            ((CanceledWhileRunning) runnable).setCanceledWhileRunning();
+            cancelableRunnable.cancel();
+          } else if (runnable instanceof CanceledWhileRunning canceledWhileRunning) {
+            canceledWhileRunning.setCanceledWhileRunning();
           }
         }
-        if (runnable instanceof Future<?>) {
+        if (runnable instanceof Future<?> future) {
           // Creating new futures eventually passes through
           // AbstractExecutorService#schedule, which will convert the Guava
           // Future to a Runnable, thereby making it impossible for the
           // cancellation to propagate from ScheduledThreadPool's task back to
           // the Guava future, so kludge it here.
-          ((Future<?>) runnable).cancel(mayInterruptIfRunning);
+          future.cancel(mayInterruptIfRunning);
         }
 
         if (isSetRunningDuringCancellation || runningState.get() == null) {
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index 0d02bb9..ed364d1 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -198,7 +199,8 @@
                   rejectCommits,
                   receiveEvent.revWalk,
                   change,
-                  skipValidation);
+                  skipValidation,
+                  /* isDirectPush= */ !MagicBranch.isMagicBranch(cmd.getRefName()));
         }
 
         validationInfos =
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 174f1ed..6d7810b 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -333,10 +333,11 @@
   }
 
   private static RestApiException asRestApiException(Exception e) {
-    if (e instanceof RestApiException) {
-      return (RestApiException) e;
-    } else if ((e instanceof ExecutionException) && (e.getCause() instanceof RestApiException)) {
-      return (RestApiException) e.getCause();
+    if (e instanceof RestApiException restApiException) {
+      return restApiException;
+    } else if ((e instanceof ExecutionException)
+        && (e.getCause() instanceof RestApiException rae)) {
+      return rae;
     }
     return RestApiException.wrap("Error inserting change/patchset", e);
   }
@@ -1827,8 +1828,7 @@
   }
 
   private static RejectionReason prohibited(AuthException e, String alreadyDisplayedResource) {
-    if (e instanceof PermissionDeniedException) {
-      PermissionDeniedException pde = (PermissionDeniedException) e;
+    if (e instanceof PermissionDeniedException pde) {
       if (pde.getResource().isPresent()
           && pde.getResource().get().equals(alreadyDisplayedResource)) {
         // Avoid repeating resource name if exactly the given name was already displayed by the
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 6d07735..8a1ef50 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -33,6 +33,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.PatchSet;
@@ -414,21 +415,23 @@
     // Disable individual emails when adding reviewers, as all reviewers will receive the single
     // bulk new change email.
     Stream<ReviewerInput> inputs =
-        Streams.concat(
-            newReviewerInputFromCommitIdentity(
-                change,
-                psInfo.getCommitId(),
-                psInfo.getAuthor().getAccount(),
-                NotifyHandling.NONE,
-                newPatchSet.uploader())
-                .stream(),
-            newReviewerInputFromCommitIdentity(
-                change,
-                psInfo.getCommitId(),
-                psInfo.getCommitter().getAccount(),
-                NotifyHandling.NONE,
-                newPatchSet.uploader())
-                .stream());
+        projectState.is(BooleanProjectConfig.SKIP_ADDING_AUTHOR_AND_COMMITTER_AS_REVIEWERS)
+            ? Stream.empty()
+            : Streams.concat(
+                newReviewerInputFromCommitIdentity(
+                    change,
+                    psInfo.getCommitId(),
+                    psInfo.getAuthor().getAccount(),
+                    NotifyHandling.NONE,
+                    newPatchSet.uploader())
+                    .stream(),
+                newReviewerInputFromCommitIdentity(
+                    change,
+                    psInfo.getCommitId(),
+                    psInfo.getCommitter().getAccount(),
+                    NotifyHandling.NONE,
+                    newPatchSet.uploader())
+                    .stream());
     if (magicBranch != null) {
       inputs =
           Streams.concat(
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 44f17a4..14fd455 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -66,6 +65,7 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectNotifyFilterValidator;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ReceiveCommitControl;
 import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
@@ -126,7 +126,7 @@
 
   @Singleton
   public static class Factory {
-    private final PersonIdent gerritIdent;
+    private final ReceiveCommitControl receiveCommitControl;
     private final DynamicItem<UrlFormatter> urlFormatter;
     private final PluginSetContext<CommitValidationListener> pluginValidators;
     private final AllUsersName allUsers;
@@ -143,7 +143,7 @@
 
     @Inject
     Factory(
-        @GerritPersonIdent PersonIdent gerritIdent,
+        ReceiveCommitControl receiveCommitControl,
         DynamicItem<UrlFormatter> urlFormatter,
         @GerritServerConfig Config config,
         PluginSetContext<CommitValidationListener> pluginValidators,
@@ -157,7 +157,7 @@
         Metrics metrics,
         ApprovalQueryBuilder approvalQueryBuilder,
         PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners) {
-      this.gerritIdent = gerritIdent;
+      this.receiveCommitControl = receiveCommitControl;
       this.urlFormatter = urlFormatter;
       this.config = config;
       this.pluginValidators = pluginValidators;
@@ -180,19 +180,23 @@
         NoteMap rejectCommits,
         RevWalk rw,
         @Nullable Change change,
-        boolean skipValidation) {
+        boolean skipValidation,
+        boolean isDirectPush) {
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
+      PermissionBackend.ForRef reviewRef = forProject.ref(MagicBranch.NEW_CHANGE + branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
-          .add(new UploadMergesPermissionValidator(perm))
+          .add(
+              new UploadMergesPermissionValidator(
+                  perm, reviewRef, isDirectPush, receiveCommitControl))
           .add(new ProjectStateValidationListener(projectState))
-          .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
-          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new AmendedGerritMergeCommitValidationListener(perm, receiveCommitControl))
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get(), receiveCommitControl))
           .add(new FileCountValidator(config, urlFormatter.get(), metrics.countManyFilesPerChange))
-          .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
-          .add(new SignedOffByValidator(user, perm, projectState))
+          .add(new CommitterUploaderValidator(user, perm, urlFormatter.get(), receiveCommitControl))
+          .add(new SignedOffByValidator(user, perm, projectState, receiveCommitControl))
           .add(new ChangeIdValidator(changeUtil, projectState, urlFormatter.get(), config, change))
           .add(
               new ConfigValidator(
@@ -226,16 +230,19 @@
         RevWalk rw,
         @Nullable Change change) {
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
+      PermissionBackend.ForRef reviewRef = forProject.ref(MagicBranch.NEW_CHANGE + branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
-          .add(new UploadMergesPermissionValidator(perm))
+          .add(
+              new UploadMergesPermissionValidator(
+                  perm, reviewRef, /* directPush= */ change == null, receiveCommitControl))
           .add(new ProjectStateValidationListener(projectState))
-          .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
-          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new AmendedGerritMergeCommitValidationListener(perm, receiveCommitControl))
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get(), receiveCommitControl))
           .add(new FileCountValidator(config, urlFormatter.get(), metrics.countManyFilesPerChange))
-          .add(new SignedOffByValidator(user, perm, projectState))
+          .add(new SignedOffByValidator(user, perm, projectState, receiveCommitControl))
           .add(new ChangeIdValidator(changeUtil, projectState, urlFormatter.get(), config, change))
           .add(
               new ConfigValidator(
@@ -277,14 +284,18 @@
       //  - Plugin validators may do things like require certain commit message
       //    formats, so we play it safe and exclude them.
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
+      PermissionBackend.ForRef reviewRef = forProject.ref(MagicBranch.NEW_CHANGE + branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
       validators
-          .add(new UploadMergesPermissionValidator(perm))
+          .add(
+              new UploadMergesPermissionValidator(
+                  perm, reviewRef, /* directPush= */ false, receiveCommitControl))
           .add(new ProjectStateValidationListener(projectState))
-          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
-          .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()));
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get(), receiveCommitControl))
+          .add(
+              new CommitterUploaderValidator(user, perm, urlFormatter.get(), receiveCommitControl));
       return new CommitValidators(commitValidationInfoListeners, validators.build());
     }
 
@@ -759,23 +770,31 @@
 
   /** Require permission to upload merge commits. */
   public static class UploadMergesPermissionValidator implements CommitValidationListener {
-    private final PermissionBackend.ForRef perm;
+    private final PermissionBackend.ForRef destRef;
+    private final PermissionBackend.ForRef reviewRef;
+    private final boolean directPush;
+    private final ReceiveCommitControl receiveCommitControl;
 
-    public UploadMergesPermissionValidator(PermissionBackend.ForRef perm) {
-      this.perm = perm;
+    public UploadMergesPermissionValidator(
+        PermissionBackend.ForRef destRef,
+        PermissionBackend.ForRef reviewRef,
+        boolean directPush,
+        ReceiveCommitControl receiveCommitControl) {
+      this.destRef = destRef;
+      this.reviewRef = reviewRef;
+      this.directPush = directPush;
+      this.receiveCommitControl = receiveCommitControl;
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      if (receiveEvent.commit.getParentCount() <= 1) {
-        return Collections.emptyList();
-      }
       try {
-        if (perm.test(RefPermission.MERGE)) {
-          return Collections.emptyList();
+        if (!receiveCommitControl.canUploadMerge(
+            destRef, reviewRef, directPush, receiveEvent.commit)) {
+          throw new CommitValidationException("you are not allowed to upload merges");
         }
-        throw new CommitValidationException("you are not allowed to upload merges");
+        return Collections.emptyList();
       } catch (PermissionBackendException e) {
         logger.atSevere().withCause(e).log("cannot check MERGE");
         throw new CommitValidationException("internal auth error");
@@ -787,12 +806,17 @@
     private final IdentifiedUser user;
     private final PermissionBackend.ForRef perm;
     private final ProjectState state;
+    private final ReceiveCommitControl receiveCommitControl;
 
     public SignedOffByValidator(
-        IdentifiedUser user, PermissionBackend.ForRef perm, ProjectState state) {
+        IdentifiedUser user,
+        PermissionBackend.ForRef perm,
+        ProjectState state,
+        ReceiveCommitControl receiveCommitControl) {
       this.user = user;
       this.perm = perm;
       this.state = state;
+      this.receiveCommitControl = receiveCommitControl;
     }
 
     @Override
@@ -821,7 +845,7 @@
       }
       if (!signedOffByAuthor && !signedOffByCommitter && !signedOffByMe) {
         try {
-          if (!perm.test(RefPermission.FORGE_COMMITTER)) {
+          if (!receiveCommitControl.canForgeCommitter(perm)) {
             throw new CommitValidationException(
                 "not Signed-off-by author/committer/uploader in message footer");
           }
@@ -839,23 +863,25 @@
     private final IdentifiedUser user;
     private final PermissionBackend.ForRef perm;
     private final UrlFormatter urlFormatter;
+    private final ReceiveCommitControl receiveCommitControl;
 
     public AuthorUploaderValidator(
-        IdentifiedUser user, PermissionBackend.ForRef perm, UrlFormatter urlFormatter) {
+        IdentifiedUser user,
+        PermissionBackend.ForRef perm,
+        UrlFormatter urlFormatter,
+        ReceiveCommitControl receiveCommitControl) {
       this.user = user;
       this.perm = perm;
       this.urlFormatter = urlFormatter;
+      this.receiveCommitControl = receiveCommitControl;
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
       PersonIdent author = receiveEvent.commit.getAuthorIdent();
-      if (user.hasEmailAddress(author.getEmailAddress())) {
-        return Collections.emptyList();
-      }
       try {
-        if (!perm.test(RefPermission.FORGE_AUTHOR)) {
+        if (!receiveCommitControl.canForgeAuthor(perm, user, receiveEvent.commit)) {
           throw new CommitValidationException(
               "invalid author", invalidEmail("author", author, user, urlFormatter));
         }
@@ -872,23 +898,25 @@
     private final IdentifiedUser user;
     private final PermissionBackend.ForRef perm;
     private final UrlFormatter urlFormatter;
+    private final ReceiveCommitControl receiveCommitControl;
 
     public CommitterUploaderValidator(
-        IdentifiedUser user, PermissionBackend.ForRef perm, UrlFormatter urlFormatter) {
+        IdentifiedUser user,
+        PermissionBackend.ForRef perm,
+        UrlFormatter urlFormatter,
+        ReceiveCommitControl receiveCommitControl) {
       this.user = user;
       this.perm = perm;
       this.urlFormatter = urlFormatter;
+      this.receiveCommitControl = receiveCommitControl;
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
       PersonIdent committer = receiveEvent.commit.getCommitterIdent();
-      if (user.hasEmailAddress(committer.getEmailAddress())) {
-        return Collections.emptyList();
-      }
       try {
-        if (!perm.test(RefPermission.FORGE_COMMITTER)) {
+        if (!receiveCommitControl.canForgeCommitter(perm, user, receiveEvent.commit)) {
           throw new CommitValidationException(
               "invalid committer", invalidEmail("committer", committer, user, urlFormatter));
         }
@@ -907,36 +935,31 @@
   public static class AmendedGerritMergeCommitValidationListener
       implements CommitValidationListener {
     private final PermissionBackend.ForRef perm;
-    private final PersonIdent gerritIdent;
+    private final ReceiveCommitControl receiveCommitControl;
 
     public AmendedGerritMergeCommitValidationListener(
-        PermissionBackend.ForRef perm, PersonIdent gerritIdent) {
+        PermissionBackend.ForRef perm, ReceiveCommitControl receiveCommitControl) {
       this.perm = perm;
-      this.gerritIdent = gerritIdent;
+      this.receiveCommitControl = receiveCommitControl;
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      PersonIdent author = receiveEvent.commit.getAuthorIdent();
-      if (receiveEvent.commit.getParentCount() > 1
-          && author.getName().equals(gerritIdent.getName())
-          && author.getEmailAddress().equals(gerritIdent.getEmailAddress())) {
-        try {
-          // Stop authors from amending the merge commits that Gerrit itself creates.
-          perm.check(RefPermission.FORGE_SERVER);
-        } catch (AuthException denied) {
-          throw new CommitValidationException(
-              String.format(
-                  "pushing merge commit %s by %s requires '%s' permission",
-                  receiveEvent.commit.getId(),
-                  gerritIdent.getEmailAddress(),
-                  RefPermission.FORGE_SERVER.name()),
-              denied);
-        } catch (PermissionBackendException e) {
-          logger.atSevere().withCause(e).log("cannot check FORGE_SERVER");
-          throw new CommitValidationException("internal auth error");
-        }
+      try {
+        // Stop authors from amending the merge commits that Gerrit itself creates.
+        receiveCommitControl.checkAmendedGerritMerge(perm, receiveEvent.commit);
+      } catch (AuthException denied) {
+        throw new CommitValidationException(
+            String.format(
+                "pushing merge commit %s by %s requires '%s' permission",
+                receiveEvent.commit.getId(),
+                receiveEvent.commit.getAuthorIdent().getEmailAddress(),
+                RefPermission.FORGE_SERVER.name()),
+            denied);
+      } catch (PermissionBackendException e) {
+        logger.atSevere().withCause(e).log("cannot check FORGE_SERVER");
+        throw new CommitValidationException("internal auth error");
       }
       return Collections.emptyList();
     }
diff --git a/java/com/google/gerrit/server/git/validators/ValidationMessage.java b/java/com/google/gerrit/server/git/validators/ValidationMessage.java
index 93cb682..b039c3f 100644
--- a/java/com/google/gerrit/server/git/validators/ValidationMessage.java
+++ b/java/com/google/gerrit/server/git/validators/ValidationMessage.java
@@ -84,8 +84,7 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (obj instanceof ValidationMessage) {
-      ValidationMessage other = (ValidationMessage) obj;
+    if (obj instanceof ValidationMessage other) {
       return Objects.equals(message, other.message) && Objects.equals(type, other.type);
     }
     return false;
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index 001a153..73dacba 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -71,8 +71,8 @@
    */
   public GroupDescription.Internal parseInternal(String id) throws UnprocessableEntityException {
     GroupDescription.Basic group = parse(id);
-    if (group instanceof GroupDescription.Internal) {
-      return (GroupDescription.Internal) group;
+    if (group instanceof GroupDescription.Internal internalGroup) {
+      return internalGroup;
     }
 
     throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
diff --git a/java/com/google/gerrit/server/group/GroupResource.java b/java/com/google/gerrit/server/group/GroupResource.java
index dfcdbd7..e999350 100644
--- a/java/com/google/gerrit/server/group/GroupResource.java
+++ b/java/com/google/gerrit/server/group/GroupResource.java
@@ -49,8 +49,8 @@
 
   public Optional<GroupDescription.Internal> asInternalGroup() {
     GroupDescription.Basic group = getGroup();
-    if (group instanceof GroupDescription.Internal) {
-      return Optional.of((GroupDescription.Internal) group);
+    if (group instanceof GroupDescription.Internal internalGroup) {
+      return Optional.of(internalGroup);
     }
     return Optional.empty();
   }
diff --git a/java/com/google/gerrit/server/index/GerritIndexStatus.java b/java/com/google/gerrit/server/index/GerritIndexStatus.java
index 3be4ab5..9f0622e 100644
--- a/java/com/google/gerrit/server/index/GerritIndexStatus.java
+++ b/java/com/google/gerrit/server/index/GerritIndexStatus.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.index;
 
 import com.google.common.primitives.Ints;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import java.io.IOException;
-import java.nio.file.Path;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
@@ -32,8 +32,10 @@
 
   private final FileBasedConfig cfg;
 
-  public GerritIndexStatus(Path indexDir) throws ConfigInvalidException, IOException {
-    cfg = new FileBasedConfig(indexDir.resolve("gerrit_index.config").toFile(), FS.detect());
+  public GerritIndexStatus(SitePaths sitePaths) throws ConfigInvalidException, IOException {
+    cfg =
+        new FileBasedConfig(
+            sitePaths.index_dir.resolve("gerrit_index.config").toFile(), FS.detect());
     cfg.load();
     convertLegacyConfig();
   }
diff --git a/java/com/google/gerrit/server/index/IndexDir.java b/java/com/google/gerrit/server/index/IndexDir.java
deleted file mode 100644
index 499c237..0000000
--- a/java/com/google/gerrit/server/index/IndexDir.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2026 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.index;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.inject.BindingAnnotation;
-import java.lang.annotation.Retention;
-
-/**
- * Marker on {@link java.nio.file.Path} holding the resolved Gerrit secondary index directory.
- *
- * <p>Honors {@code index.directory} in {@code gerrit.config} if set, defaulting to {@code
- * site_path/index} otherwise.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface IndexDir {}
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index c29c08e..7b22f3a 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 
-import com.google.common.base.Strings;
 import com.google.common.base.Ticker;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
@@ -36,7 +35,6 @@
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.WorkQueue;
@@ -67,14 +65,9 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.google.inject.multibindings.OptionalBinder;
-import java.io.IOException;
-import java.nio.file.Path;
 import java.util.Collection;
 import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 
 /**
  * Module for non-indexer-specific secondary index setup.
@@ -84,7 +77,6 @@
  */
 @SuppressWarnings("ProvidesMethodOutsideOfModule")
 public class IndexModule extends LifecycleModule {
-  public static final String INDEX = "index";
   public static final ImmutableList<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
       ImmutableList.of(
           AccountSchemaDefinitions.INSTANCE,
@@ -95,7 +87,7 @@
   /** Type of secondary index. */
   public static IndexType getIndexType(Injector injector) {
     Config cfg = injector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    String configValue = cfg != null ? cfg.getString(INDEX, null, "type") : null;
+    String configValue = cfg != null ? cfg.getString("index", null, "type") : null;
     return new IndexType(configValue);
   }
 
@@ -227,7 +219,8 @@
     int threads = this.threads;
     if (threads == 0) {
       threads =
-          config.getInt(INDEX, null, "threads", Runtime.getRuntime().availableProcessors() / 2 + 1);
+          config.getInt(
+              "index", null, "threads", Runtime.getRuntime().availableProcessors() / 2 + 1);
     }
     if (threads < 0) {
       return MoreExecutors.newDirectExecutorService();
@@ -247,7 +240,7 @@
     int threads = this.threads;
     if (threads == 0) {
       threads =
-          config.getInt(INDEX, null, "batchThreads", Runtime.getRuntime().availableProcessors());
+          config.getInt("index", null, "batchThreads", Runtime.getRuntime().availableProcessors());
     }
     if (threads < 0) {
       return MoreExecutors.newDirectExecutorService();
@@ -262,35 +255,6 @@
     return new StalenessChecker(indexes, repoManager, indexConfig);
   }
 
-  @Provides
-  @Singleton
-  @IndexDir
-  Path getIndexDirectory(@GerritServerConfig Config cfg, SitePaths site) {
-    return indexDirectory(cfg, site);
-  }
-
-  /**
-   * Resolves the index directory before the Guice injector exists (e.g. during {@code Init}), by
-   * reading {@code gerrit.config} directly from disk.
-   */
-  public static Path indexDirectory(SitePaths sitePaths) throws IOException {
-    FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
-    if (cfg.getFile().exists()) {
-      try {
-        cfg.load();
-      } catch (ConfigInvalidException e) {
-        throw new IOException("Invalid config file " + sitePaths.gerrit_config, e);
-      }
-    }
-    return indexDirectory(cfg, sitePaths);
-  }
-
-  /** Resolves the index directory from an already-loaded {@code gerrit.config}. */
-  public static Path indexDirectory(Config cfg, SitePaths site) {
-    String name = cfg.getString(INDEX, null, "directory");
-    return Strings.isNullOrEmpty(name) ? site.resolve(INDEX) : site.resolve(name);
-  }
-
   @Singleton
   private static class ShutdownIndexExecutors implements LifecycleListener {
     private final ListeningExecutorService interactiveExecutor;
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 8c07af3..f81a9ce 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -25,11 +25,11 @@
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.group.GroupField;
 import com.google.gerrit.server.query.change.GroupBackedUser;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -37,9 +37,9 @@
 public final class IndexUtils {
 
   /** Mark an index version as ready to serve queries. */
-  public static void setReady(Path indexDir, String name, int version, boolean ready) {
+  public static void setReady(SitePaths sitePaths, String name, int version, boolean ready) {
     try {
-      GerritIndexStatus cfg = new GerritIndexStatus(indexDir);
+      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
       cfg.setReady(name, version, ready);
       cfg.save();
     } catch (ConfigInvalidException | IOException e) {
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java
index e3c1c96..d4f55ba 100644
--- a/java/com/google/gerrit/server/index/VersionManager.java
+++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.ProvisionException;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -100,7 +99,6 @@
   protected final boolean reuseExistingDocuments;
   protected final String runReindexMsg;
   protected final SitePaths sitePaths;
-  protected final Path indexDir;
 
   private final PluginSetContext<OnlineUpgradeListener> listeners;
 
@@ -110,13 +108,11 @@
 
   protected VersionManager(
       SitePaths sitePaths,
-      Path indexDir,
       PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs,
       boolean onlineUpgrade,
       boolean reuseExistingDocuments) {
     this.sitePaths = sitePaths;
-    this.indexDir = indexDir;
     this.listeners = listeners;
     this.defs = Maps.newHashMapWithExpectedSize(defs.size());
     for (IndexDefinition<?, ?, ?> def : defs) {
@@ -271,7 +267,7 @@
 
   protected GerritIndexStatus createIndexStatus() {
     try {
-      return new GerritIndexStatus(indexDir);
+      return new GerritIndexStatus(sitePaths);
     } catch (ConfigInvalidException | IOException e) {
       throw fail(e);
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 0a743de..a40ec11 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -95,8 +95,8 @@
   }
 
   private static @Nullable EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
-    if (in instanceof ChangeStatusPredicate) {
-      Status status = ((ChangeStatusPredicate) in).getStatus();
+    if (in instanceof ChangeStatusPredicate changeStatusPredicate) {
+      Status status = changeStatusPredicate.getStatus();
       return status != null ? EnumSet.of(status) : null;
     } else if (in instanceof NotPredicate) {
       EnumSet<Status> s = extractStatus(in.getChild(0));
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 6073823..9092932 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritInstanceName;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -55,6 +56,7 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 import org.apache.http.client.utils.URIBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -104,6 +106,7 @@
   public final String instanceName;
   public final Provider<CurrentUser> currentUserProvider;
   public final RetryHelper retryHelper;
+  public final ExecutorService sendEmailExecutor;
 
   @Inject
   EmailArguments(
@@ -137,7 +140,8 @@
       @GerritInstanceName String instanceName,
       @GerritServerConfig Config cfg,
       Provider<CurrentUser> currentUserProvider,
-      RetryHelper retryHelper) {
+      RetryHelper retryHelper,
+      @SendEmailExecutor ExecutorService sendEmailExecutor) {
     this.server = server;
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
@@ -169,6 +173,7 @@
     this.addInstanceNameInSubject = cfg.getBoolean("sendemail", "addInstanceNameInSubject", false);
     this.currentUserProvider = currentUserProvider;
     this.retryHelper = retryHelper;
+    this.sendEmailExecutor = sendEmailExecutor;
   }
 
   /** Fetch ChangeData for the specified change. */
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 64c6657..0572ef8 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Account;
@@ -58,6 +59,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.StringJoiner;
+import java.util.concurrent.Future;
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.SystemReader;
@@ -182,7 +184,7 @@
     return soyHtmlTemplate("EmailHtml").toString();
   }
 
-  /** Format and enqueue the message for delivery. */
+  /** Format and send the message synchronously. */
   public void send() throws EmailException {
     try {
       @SuppressWarnings("unused")
@@ -204,6 +206,50 @@
     }
   }
 
+  /**
+   * Send the email asynchronously via the injected SendEmailExecutor.
+   *
+   * <p>This method submits the email sending task to a background thread pool, allowing the caller
+   * to continue without waiting for SMTP operations to complete. Exception handling and logging are
+   * handled automatically within the async task.
+   *
+   * <p>The executor is injected via EmailArguments and configured through {@code
+   * sendemail.threadPoolSize} in gerrit.config.
+   *
+   * @return Future representing the async operation (return value can be ignored)
+   */
+  @CanIgnoreReturnValue
+  public Future<?> sendAsync() {
+    AsyncEmailSender sender = new AsyncEmailSender(this);
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError = args.sendEmailExecutor.submit(sender);
+    return possiblyIgnoredError;
+  }
+
+  /** Async email sender that wraps the synchronous send() call with proper exception handling. */
+  private static class AsyncEmailSender implements Runnable {
+    private final OutgoingEmail email;
+
+    AsyncEmailSender(OutgoingEmail email) {
+      this.email = email;
+    }
+
+    @Override
+    public void run() {
+      try {
+        email.send();
+      } catch (Exception e) {
+        logger.atSevere().withCause(e).log(
+            "Failed to send email asynchronously (class: %s)", email.messageClass);
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "send-email " + email.messageClass;
+    }
+  }
+
   private void sendImpl() throws EmailException {
     if (!args.emailSender.isEnabled()) {
       // Server has explicitly disabled email sending.
@@ -831,8 +877,8 @@
     }
     for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) {
       // Don't remove fromEmail from the "From" header though!
-      if (entry.getValue() instanceof AddressList && !entry.getKey().equals("From")) {
-        ((AddressList) entry.getValue()).remove(fromEmail);
+      if (entry.getValue() instanceof AddressList addressList && !entry.getKey().equals("From")) {
+        addressList.remove(fromEmail);
       }
     }
   }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
index e28a77d..3599473 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
@@ -16,11 +16,12 @@
 
 import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS;
 
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.List;
 import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.commons.validator.routines.DomainValidator.Item;
 import org.apache.commons.validator.routines.EmailValidator;
 import org.eclipse.jgit.lib.Config;
 
@@ -33,23 +34,19 @@
  */
 @Singleton
 public class OutgoingEmailValidator {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final EmailValidator validator;
 
   @Inject
   OutgoingEmailValidator(@GerritServerConfig Config config) {
     String[] allowTLD = config.getStringList("sendemail", null, "allowTLD");
-    if (allowTLD.length != 0) {
-      try {
-        DomainValidator.updateTLDOverride(GENERIC_PLUS, allowTLD);
-      } catch (IllegalStateException e) {
-        // Should only happen in tests, where the OutgoingEmailValidator
-        // is instantiated repeatedly.
-        logger.atSevere().log("Failed to update TLD override: %s", e.getMessage());
-      }
-    }
+    DomainValidator domainValidator =
+        allowTLD.length == 0
+            ? DomainValidator.getInstance(true)
+            : DomainValidator.getInstance(true, List.of(new Item(GENERIC_PLUS, allowTLD)));
+    this.validator = new EmailValidator(true, true, domainValidator);
   }
 
   public boolean isValid(String addr) {
-    return EmailValidator.getInstance(true, true).isValid(addr);
+    return validator.isValid(addr);
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index c050c13..c23f208 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -121,18 +121,11 @@
     smtpEncryption = cfg.getEnum("sendemail", null, "smtpencryption", Encryption.NONE);
     sslVerify = cfg.getBoolean("sendemail", null, "sslverify", true);
 
-    final int defaultPort;
-    switch (smtpEncryption) {
-      case SSL:
-        defaultPort = 465;
-        break;
-
-      case NONE:
-      case TLS:
-      default:
-        defaultPort = 25;
-        break;
-    }
+    final int defaultPort =
+        switch (smtpEncryption) {
+          case SSL -> 465;
+          case NONE, TLS -> 25;
+        };
     smtpPort = cfg.getInt("sendemail", null, "smtpserverport", defaultPort);
 
     smtpUser = cfg.getString("sendemail", null, "smtpuser");
@@ -451,8 +444,8 @@
           // Ignored
         }
       }
-      if (e instanceof EmailException) {
-        throw (EmailException) e;
+      if (e instanceof EmailException emailException) {
+        throw emailException;
       }
       throw new EmailException(e.getMessage(), e);
     }
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 3aed6b0..2d917ac 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -222,8 +222,8 @@
     Optional<ObjectId> commit = repoView.getRef(refName);
     if (commit.isPresent()) {
       RevObject obj = repoView.getRevWalk().parseAny(commit.get());
-      if (obj instanceof RevCommit) {
-        return Optional.of((RevCommit) obj);
+      if (obj instanceof RevCommit revCommit) {
+        return Optional.of(revCommit);
       }
     }
     return Optional.empty();
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryKey.java b/java/com/google/gerrit/server/patch/DiffSummaryKey.java
index 1f4fa07..3e2e0dc 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryKey.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryKey.java
@@ -62,8 +62,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof DiffSummaryKey) {
-      DiffSummaryKey k = (DiffSummaryKey) o;
+    if (o instanceof DiffSummaryKey k) {
       return Objects.equals(oldId, k.oldId)
           && Objects.equals(parentNum, k.parentNum)
           && Objects.equals(newId, k.newId)
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiff.java b/java/com/google/gerrit/server/patch/IntraLineDiff.java
index fb13207..6c79761 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -84,8 +84,7 @@
     for (Edit e : edits) {
       writeEdit(out, e);
 
-      if (e instanceof ReplaceEdit) {
-        ReplaceEdit r = (ReplaceEdit) e;
+      if (e instanceof ReplaceEdit r) {
         writeVarInt32(out, r.getInternalEdits().size());
         for (Edit i : r.getInternalEdits()) {
           writeEdit(out, i);
@@ -120,8 +119,8 @@
   }
 
   private static Edit copy(Edit edit) {
-    if (edit instanceof ReplaceEdit) {
-      return copy((ReplaceEdit) edit);
+    if (edit instanceof ReplaceEdit replaceEdit) {
+      return copy(replaceEdit);
     }
     return new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB());
   }
diff --git a/java/com/google/gerrit/server/patch/MagicFile.java b/java/com/google/gerrit/server/patch/MagicFile.java
index ec4b9d8..bb779e4 100644
--- a/java/com/google/gerrit/server/patch/MagicFile.java
+++ b/java/com/google/gerrit/server/patch/MagicFile.java
@@ -35,8 +35,8 @@
       throws IOException {
     try (RevWalk rw = new RevWalk(reader)) {
       RevCommit c;
-      if (commitId instanceof RevCommit) {
-        c = (RevCommit) commitId;
+      if (commitId instanceof RevCommit revCommit) {
+        c = revCommit;
       } else {
         c = rw.parseCommit(commitId);
       }
diff --git a/java/com/google/gerrit/server/patch/PatchListKey.java b/java/com/google/gerrit/server/patch/PatchListKey.java
index 386c50d..29bfe6a 100644
--- a/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -136,8 +136,7 @@
 
   @Override
   public boolean equals(Object o) {
-    if (o instanceof PatchListKey) {
-      PatchListKey k = (PatchListKey) o;
+    if (o instanceof PatchListKey k) {
       return Objects.equals(oldId, k.oldId)
           && Objects.equals(parentNum, k.parentNum)
           && Objects.equals(newId, k.newId)
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 9abbe45..426928e 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -438,11 +438,11 @@
                 FileDiffOutput.createExpensive(
                     key.newFilePath(), key.oldCommit(), key.newCommit()));
           } catch (ExecutionException e) {
-            if (e.getCause() instanceof DiffNotAvailableException) {
-              throw (DiffNotAvailableException) e.getCause();
+            if (e.getCause() instanceof DiffNotAvailableException dnae) {
+              throw dnae;
             }
-            if (e.getCause() instanceof IOException) {
-              throw (IOException) e.getCause();
+            if (e.getCause() instanceof IOException ioe) {
+              throw ioe;
             }
             throw new DiffNotAvailableException(e);
           }
diff --git a/java/com/google/gerrit/server/permissions/AbstractChangeControl.java b/java/com/google/gerrit/server/permissions/AbstractChangeControl.java
index 07a0833..2632fc25 100644
--- a/java/com/google/gerrit/server/permissions/AbstractChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/AbstractChangeControl.java
@@ -70,14 +70,13 @@
   }
 
   protected boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
-    if (perm instanceof ChangePermission) {
-      return can((ChangePermission) perm);
-    } else if (perm instanceof AbstractLabelPermission) {
-      return can((AbstractLabelPermission) perm);
-    } else if (perm instanceof AbstractLabelPermission.WithValue) {
-      return can((AbstractLabelPermission.WithValue) perm);
-    }
-    throw new PermissionBackendException(perm + " unsupported");
+    return switch (perm) {
+      case ChangePermission changePermission -> can(changePermission);
+      case AbstractLabelPermission abstractLabelPermission -> can(abstractLabelPermission);
+      case AbstractLabelPermission.WithValue labelPermissionWithValue ->
+          can(labelPermissionWithValue);
+      case null, default -> throw new PermissionBackendException(perm + " unsupported");
+    };
   }
 
   protected boolean can(ChangePermission perm) throws PermissionBackendException {
@@ -108,9 +107,14 @@
     }
   }
 
-  /** Can this user perform AI review for this change? */
-  private boolean canAiReview() {
-    return refControl.canPerformDefaultAllow(Permission.AI_REVIEW);
+  /**
+   * Whether the user may use AI Review on this change. The base check is the standard default-deny
+   * model (an explicit {@code aiReview} rule is required). {@link ChangeControl} overrides this to
+   * also allow registered users when the {@code allow_ai_review_for_registered_users} experiment is
+   * enabled, which overrides the ACL (including a {@code block}) for identified users.
+   */
+  protected boolean canAiReview() {
+    return refControl.canPerform(Permission.AI_REVIEW);
   }
 
   /** Can this user see this change? */
@@ -300,9 +304,9 @@
   }
 
   protected static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
-    if (permSet instanceof EnumSet) {
+    if (permSet instanceof EnumSet enumSet) {
       @SuppressWarnings({"unchecked", "rawtypes"})
-      Set<T> s = ((EnumSet) permSet).clone();
+      Set<T> s = enumSet.clone();
       s.clear();
       return s;
     }
diff --git a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
index ba7caed..a1a500d 100644
--- a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
@@ -21,7 +21,8 @@
 import com.google.gerrit.server.util.LabelVote;
 
 /** Abstract permission representing a label. */
-public abstract class AbstractLabelPermission implements ChangePermissionOrLabel {
+public abstract sealed class AbstractLabelPermission implements ChangePermissionOrLabel
+    permits LabelPermission, LabelRemovalPermission {
   public enum ForUser {
     SELF,
     ON_BEHALF_OF
@@ -90,7 +91,8 @@
   }
 
   /** A {@link AbstractLabelPermission} at a specific value. */
-  public abstract static class WithValue implements ChangePermissionOrLabel {
+  public abstract static sealed class WithValue implements ChangePermissionOrLabel
+      permits LabelPermission.WithValue, LabelRemovalPermission.WithValue {
     private final ForUser forUser;
     private final LabelVote label;
 
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 3941975..5057190 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
@@ -34,10 +36,12 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ChangeData changeData;
+  private final ExperimentFeatures experimentFeatures;
 
   @Inject
   protected ChangeControl(
       PermissionBackend permissionBackend,
+      ExperimentFeatures experimentFeatures,
       @Assisted ProjectControl projectControl,
       @Assisted RefControl refControl,
       @Assisted ChangeData changeData) {
@@ -48,6 +52,7 @@
         changeData.change().isNew(),
         isOwner(refControl, changeData));
     this.changeData = changeData;
+    this.experimentFeatures = experimentFeatures;
   }
 
   private static boolean isOwner(RefControl refControl, ChangeData changeData) {
@@ -64,6 +69,14 @@
     return new ForChangeImpl(changeData.getId());
   }
 
+  @Override
+  protected boolean canAiReview() {
+    return super.canAiReview()
+        || (experimentFeatures.isFeatureEnabled(
+                ExperimentFeaturesConstants.ALLOW_AI_REVIEW_FOR_REGISTERED_USERS)
+            && getUser().isIdentifiedUser());
+  }
+
   /** Can this user see this change? */
   @Override
   protected boolean isVisible() {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 0e5820a..a0e18fb 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -178,14 +178,13 @@
     }
 
     private boolean can(GlobalOrPluginPermission perm) throws PermissionBackendException {
-      if (perm instanceof GlobalPermission) {
-        return can((GlobalPermission) perm);
-      } else if (perm instanceof PluginPermission) {
-        PluginPermission pluginPermission = (PluginPermission) perm;
-        return has(DefaultPermissionMappings.pluginCapabilityName(pluginPermission))
-            || (pluginPermission.fallBackToAdmin() && isAdmin());
-      }
-      throw new PermissionBackendException(perm + " unsupported");
+      return switch (perm) {
+        case GlobalPermission globalPermission -> can(globalPermission);
+        case PluginPermission pluginPermission ->
+            has(DefaultPermissionMappings.pluginCapabilityName(pluginPermission))
+                || (pluginPermission.fallBackToAdmin() && isAdmin());
+        case null, default -> throw new PermissionBackendException(perm + " unsupported");
+      };
     }
 
     private boolean can(GlobalPermission perm) throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index 4db3add..2bdf97d 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -193,28 +193,22 @@
   }
 
   public static String labelPermissionName(AbstractLabelPermission labelPermission) {
-    if (labelPermission instanceof LabelPermission) {
-      if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
-        return Permission.forLabelAs(labelPermission.label());
-      }
-      return Permission.forLabel(labelPermission.label());
-    } else if (labelPermission instanceof LabelRemovalPermission) {
-      return Permission.forRemoveLabel(labelPermission.label());
-    }
-    throw new IllegalStateException("invalid AbstractLabelPermission subtype");
+    return switch (labelPermission) {
+      case LabelPermission lp when lp.forUser() == ForUser.ON_BEHALF_OF ->
+          Permission.forLabelAs(lp.label());
+      case LabelPermission lp -> Permission.forLabel(lp.label());
+      case LabelRemovalPermission lrp -> Permission.forRemoveLabel(lrp.label());
+    };
   }
 
   // TODO(dborowitz): Can these share a common superinterface?
   public static String labelPermissionName(AbstractLabelPermission.WithValue labelPermission) {
-    if (labelPermission instanceof LabelPermission.WithValue) {
-      if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
-        return Permission.forLabelAs(labelPermission.label());
-      }
-      return Permission.forLabel(labelPermission.label());
-    } else if (labelPermission instanceof LabelRemovalPermission.WithValue) {
-      return Permission.forRemoveLabel(labelPermission.label());
-    }
-    throw new IllegalStateException("invalid AbstractLabelPermission.WithValue subtype");
+    return switch (labelPermission) {
+      case LabelPermission.WithValue lp when lp.forUser() == ForUser.ON_BEHALF_OF ->
+          Permission.forLabelAs(lp.label());
+      case LabelPermission.WithValue lp -> Permission.forLabel(lp.label());
+      case LabelRemovalPermission.WithValue lrp -> Permission.forRemoveLabel(lrp.label());
+    };
   }
 
   private DefaultPermissionMappings() {}
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index 4652364..62f49ae 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.util.LabelVote;
 
 /** Permission representing a label. */
-public class LabelPermission extends AbstractLabelPermission {
+public non-sealed class LabelPermission extends AbstractLabelPermission {
   /**
    * Construct a reference to a label permission.
    *
@@ -66,7 +66,7 @@
   }
 
   /** A {@link LabelPermission} at a specific value. */
-  public static class WithValue extends AbstractLabelPermission.WithValue {
+  public static non-sealed class WithValue extends AbstractLabelPermission.WithValue {
     /**
      * Construct a reference to a label at a specific value.
      *
diff --git a/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
index 2553601..ea682e0 100644
--- a/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.util.LabelVote;
 
 /** Permission representing a label removal. */
-public class LabelRemovalPermission extends AbstractLabelPermission {
+public non-sealed class LabelRemovalPermission extends AbstractLabelPermission {
   /**
    * Construct a reference to a label removal permission.
    *
@@ -46,7 +46,7 @@
   }
 
   /** A {@link LabelRemovalPermission} at a specific value. */
-  public static class WithValue extends AbstractLabelPermission.WithValue {
+  public static non-sealed class WithValue extends AbstractLabelPermission.WithValue {
     /**
      * Construct a reference to a label removal at a specific value.
      *
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 93be379..dc9d020 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -333,8 +333,8 @@
         throws PermissionBackendException;
 
     public boolean test(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
-      if (perm instanceof ProjectPermission) {
-        return test(EnumSet.of((ProjectPermission) perm)).contains(perm);
+      if (perm instanceof ProjectPermission projectPermission) {
+        return test(EnumSet.of(projectPermission)).contains(perm);
       }
 
       // TODO(xchangcheng): implement for plugin defined project permissions.
diff --git a/java/com/google/gerrit/server/permissions/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index 4847c14..e2d7857 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -101,8 +101,8 @@
         // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
         // that will never be shared with non-user references, and the per-user
         // references are usually less frequent than the non-user references.
-        if (sm.getMatcher() instanceof ExpandParameters) {
-          if (!((ExpandParameters) sm.getMatcher()).matchPrefix(ref)) {
+        if (sm.getMatcher() instanceof ExpandParameters ep) {
+          if (!ep.matchPrefix(ref)) {
             continue;
           }
           perUser = true;
@@ -171,23 +171,6 @@
     }
   }
 
-  /** Returns DENY rules for the given permission. */
-  List<PermissionRule> getDenyRules(String perm) {
-    List<PermissionRule> result = new ArrayList<>();
-    for (AccessSection s : accessSectionsUpward) {
-      Permission p = s.getPermission(perm);
-      if (p == null) {
-        continue;
-      }
-      for (PermissionRule pr : p.getRules()) {
-        if (pr.getAction() == PermissionRule.Action.DENY) {
-          result.add(pr);
-        }
-      }
-    }
-    return result;
-  }
-
   /** Returns permissions in the right order for evaluating BLOCK status. */
   List<List<Permission>> getBlockRules(String perm) {
     List<List<Permission>> ps = blockPerProjectByPermission.get(perm);
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 9cab8b2..9eceeb6 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -463,14 +463,13 @@
     }
 
     private boolean can(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
-      if (perm instanceof ProjectPermission) {
-        return can((ProjectPermission) perm);
-      } else if (perm instanceof PluginProjectPermission) {
+      return switch (perm) {
+        case ProjectPermission projectPermission -> can(projectPermission);
         // TODO(xchangcheng): implement for plugin defined project permissions.
-        return false;
-      }
-
-      throw new PermissionBackendException(perm.describeForException() + " unsupported");
+        case PluginProjectPermission _ -> false;
+        case null, default ->
+            throw new PermissionBackendException(perm.describeForException() + " unsupported");
+      };
     }
 
     private boolean can(ProjectPermission perm) {
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 6b081d8..b1b664c 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -198,39 +198,6 @@
     return canPerform(permissionName, false, false);
   }
 
-  /**
-   * Default-allow variant of {@link #canPerform}: grants access unless explicitly restricted.
-   *
-   * <p>Unlike standard Gerrit permissions which are default-deny, this method defaults to granting
-   * access. It denies when the user matches an explicit DENY or BLOCK rule. When ALLOW rules are
-   * configured, it falls back to standard {@link #canPerform} evaluation.
-   *
-   * <p>This is used for opt-out permissions such as AI Review, where access is available by default
-   * unless an admin explicitly restricts it.
-   */
-  boolean canPerformDefaultAllow(String permissionName) {
-    if (!relevant.getAllowRules(permissionName).isEmpty()) {
-      return canPerform(permissionName);
-    }
-    for (PermissionRule pr : relevant.getDenyRules(permissionName)) {
-      if (projectControl.match(pr, false)) {
-        if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
-          String logMessage =
-              String.format(
-                  "'%s' cannot perform '%s' on project '%s' for ref '%s'"
-                      + " because this permission is denied",
-                  getUser().getLoggableName(),
-                  permissionName,
-                  projectControl.getProject().getName(),
-                  refName);
-          LoggingContext.getInstance().addAclLogRecord(logMessage);
-        }
-        return false;
-      }
-    }
-    return !isBlocked(permissionName, false, false);
-  }
-
   ForRef asForRef() {
     return new ForRefImpl();
   }
@@ -243,11 +210,6 @@
     return canPerform(Permission.REVERT);
   }
 
-  /** Returns true if this user can submit merge patch sets to this ref */
-  private boolean canUploadMerges() {
-    return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
-  }
-
   /** Returns true if the user can update the reference as a fast-forward. */
   private boolean canUpdate() {
     if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
@@ -713,7 +675,7 @@
         return canForgeGerritServerIdentity();
       }
       case MERGE -> {
-        return canUploadMerges();
+        return canPerform(Permission.PUSH_MERGE);
       }
       case CREATE_CHANGE -> {
         return canUpload();
@@ -740,7 +702,7 @@
         return canForgeAuthor()
             && canForgeCommitter()
             && canForgeGerritServerIdentity()
-            && canUploadMerges();
+            && canPerform(Permission.PUSH_MERGE);
       }
     }
     throw new PermissionBackendException(perm + " unsupported");
diff --git a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index 1942193..2b5463b 100644
--- a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -198,10 +198,10 @@
   private void listen(java.lang.reflect.Type type, Class<?> clazz) throws InvalidPluginException {
     while (type != null) {
       Class<?> rawType;
-      if (type instanceof ParameterizedType) {
-        rawType = (Class<?>) ((ParameterizedType) type).getRawType();
-      } else if (type instanceof Class) {
-        rawType = (Class<?>) type;
+      if (type instanceof ParameterizedType parameterizedType) {
+        rawType = (Class<?>) parameterizedType.getRawType();
+      } else if (type instanceof Class<?> typeClass) {
+        rawType = typeClass;
       } else {
         return;
       }
diff --git a/java/com/google/gerrit/server/plugins/Plugin.java b/java/com/google/gerrit/server/plugins/Plugin.java
index 3de7e27..917c931 100644
--- a/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/java/com/google/gerrit/server/plugins/Plugin.java
@@ -150,11 +150,11 @@
 
   public void add(RegistrationHandle handle) {
     if (manager != null) {
-      if (handle instanceof ReloadableRegistrationHandle) {
+      if (handle instanceof ReloadableRegistrationHandle<?> reloadableHandle) {
         if (reloadableHandles == null) {
           reloadableHandles = new ArrayList<>();
         }
-        reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+        reloadableHandles.add(reloadableHandle);
       }
       manager.add(handle);
     }
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 18ab2ab..01b2c03 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -347,11 +347,11 @@
   @Override
   public void add(RegistrationHandle handle) {
     if (serverManager != null) {
-      if (handle instanceof ReloadableRegistrationHandle) {
+      if (handle instanceof ReloadableRegistrationHandle<?> reloadableHandle) {
         if (reloadableHandles == null) {
           reloadableHandles = new ArrayList<>();
         }
-        reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+        reloadableHandles.add(reloadableHandle);
       }
       serverManager.add(handle);
     }
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 955ff95..19babe6 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -101,10 +101,10 @@
     ps.checkStatePermitsWrite();
 
     PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(destBranch);
-    if (object instanceof RevCommit) {
+    if (object instanceof RevCommit commit) {
       perm.check(RefPermission.CREATE);
       if (sourceBranches.length == 0) {
-        checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm, forPush);
+        checkCreateCommit(user, repo, commit, ps.getNameKey(), perm, forPush);
       } else {
         List<Ref> sourceRefs = new ArrayList<>();
         for (BranchNameKey src : sourceBranches) {
@@ -113,7 +113,7 @@
         if (reachable.fromRefs(
             destBranch.project(),
             repo,
-            (RevCommit) object,
+            commit,
             ImmutableList.copyOf(sourceRefs),
             Optional.of(user.get()))) {
           return;
@@ -128,8 +128,7 @@
       }
       return;
     }
-    if (object instanceof RevTag) {
-      RevTag tag = (RevTag) object;
+    if (object instanceof RevTag tag) {
       try (RevWalk rw = new RevWalk(repo)) {
         rw.parseBody(tag);
       } catch (IOException e) {
@@ -147,8 +146,8 @@
       }
 
       RevObject target = tag.getObject();
-      if (target instanceof RevCommit) {
-        checkCreateCommit(user, repo, (RevCommit) target, ps.getNameKey(), perm, forPush);
+      if (target instanceof RevCommit revCommit) {
+        checkCreateCommit(user, repo, revCommit, ps.getNameKey(), perm, forPush);
       } else {
         checkCreateRef(user, repo, destBranch, target, forPush);
       }
diff --git a/java/com/google/gerrit/server/project/ReceiveCommitControl.java b/java/com/google/gerrit/server/project/ReceiveCommitControl.java
new file mode 100644
index 0000000..a63c427
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ReceiveCommitControl.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Access control for a commit entering Gerrit's commit-validation pipeline.
+ *
+ * <p>Owns the push-permission decisions that depend on the {@link RevCommit} being pushed: whether
+ * the caller may forge the author, committer, or server identity, or upload a merge commit. Like
+ * {@link CreateRefControl}, it combines Git object data with a {@link PermissionBackend.ForRef} and
+ * keeps the decision in the project access-control layer rather than inside the commit validators.
+ * The validators translate the results into {@code CommitValidationException}s and render any
+ * user-facing messages.
+ */
+@Singleton
+public class ReceiveCommitControl {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final PersonIdent gerritIdent;
+
+  @Inject
+  ReceiveCommitControl(@GerritPersonIdent PersonIdent gerritIdent) {
+    this.gerritIdent = gerritIdent;
+  }
+
+  /** Whether the user may push a commit whose author identity they do not own. */
+  public boolean canForgeAuthor(
+      PermissionBackend.ForRef forRef, IdentifiedUser user, RevCommit commit)
+      throws PermissionBackendException {
+    if (user.hasEmailAddress(commit.getAuthorIdent().getEmailAddress())) {
+      return true;
+    }
+    return forRef.test(RefPermission.FORGE_AUTHOR);
+  }
+
+  /** Whether the user may push a commit whose committer identity they do not own. */
+  public boolean canForgeCommitter(
+      PermissionBackend.ForRef forRef, IdentifiedUser user, RevCommit commit)
+      throws PermissionBackendException {
+    if (user.hasEmailAddress(commit.getCommitterIdent().getEmailAddress())) {
+      return true;
+    }
+    return forRef.test(RefPermission.FORGE_COMMITTER);
+  }
+
+  /** Whether the committer identity may be forged on this ref. */
+  public boolean canForgeCommitter(PermissionBackend.ForRef forRef)
+      throws PermissionBackendException {
+    return forRef.test(RefPermission.FORGE_COMMITTER);
+  }
+
+  /**
+   * Whether the user may push this commit if it is a merge; non-merges are always allowed.
+   *
+   * <p>Direct pushes are authorized by {@code Push Merge Commit} on the destination ref. For
+   * backward compatibility a grant on the {@code refs/for/} review ref also still authorizes direct
+   * pushes, but that fallback is deprecated and will be removed in a future release. Pushes for
+   * review are authorized by the grant on the {@code refs/for/} review ref.
+   */
+  public boolean canUploadMerge(
+      PermissionBackend.ForRef destRef,
+      PermissionBackend.ForRef reviewRef,
+      boolean directPush,
+      RevCommit commit)
+      throws PermissionBackendException {
+    if (commit.getParentCount() <= 1) {
+      return true;
+    }
+    if (directPush) {
+      if (destRef.test(RefPermission.MERGE)) {
+        return true;
+      }
+      if (reviewRef.test(RefPermission.MERGE)) {
+        logger.atWarning().atMostEvery(1, TimeUnit.HOURS).log(
+            "Direct merge push to %s was authorized only via the deprecated refs/for Push Merge"
+                + " Commit fallback; grant Push Merge Commit on the destination ref instead. This"
+                + " fallback will be removed in a future release.",
+            destRef.resourcePath());
+        return true;
+      }
+      return false;
+    }
+    return reviewRef.test(RefPermission.MERGE);
+  }
+
+  /**
+   * Rejects amending a merge commit authored by the Gerrit server identity unless the caller may
+   * forge the server identity. No-op for commits that are not Gerrit-authored merges.
+   */
+  public void checkAmendedGerritMerge(PermissionBackend.ForRef forRef, RevCommit commit)
+      throws AuthException, PermissionBackendException {
+    PersonIdent author = commit.getAuthorIdent();
+    if (commit.getParentCount() > 1
+        && author.getName().equals(gerritIdent.getName())
+        && author.getEmailAddress().equals(gerritIdent.getEmailAddress())) {
+      forRef.check(RefPermission.FORGE_SERVER);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/AndChangeSource.java b/java/com/google/gerrit/server/query/change/AndChangeSource.java
index 755ca69..ae41a36 100644
--- a/java/com/google/gerrit/server/query/change/AndChangeSource.java
+++ b/java/com/google/gerrit/server/query/change/AndChangeSource.java
@@ -39,9 +39,7 @@
   @Override
   public int compare(Predicate<ChangeData> a, Predicate<ChangeData> b) {
     int cmp = super.compare(a, b);
-    if (cmp == 0 && a instanceof ChangeDataSource && b instanceof ChangeDataSource) {
-      ChangeDataSource as = (ChangeDataSource) a;
-      ChangeDataSource bs = (ChangeDataSource) b;
+    if (cmp == 0 && a instanceof ChangeDataSource as && b instanceof ChangeDataSource bs) {
       cmp = (as.hasChange() ? 0 : 1) - (bs.hasChange() ? 0 : 1);
     }
     return cmp;
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index 8894287..a137ae4 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -81,8 +81,8 @@
     if (cardinality < 0) {
       cardinality = 0;
       for (Predicate<ChangeData> p : getChildren()) {
-        if (p instanceof ChangeDataSource) {
-          cardinality += ((ChangeDataSource) p).getCardinality();
+        if (p instanceof ChangeDataSource changeDataSource) {
+          cardinality += changeDataSource.getCardinality();
         }
       }
     }
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index cc186c3..25a54b7 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -392,9 +392,9 @@
     switch (outputFormat) {
       default:
       case TEXT:
-        if (data instanceof ChangeAttribute) {
+        if (data instanceof ChangeAttribute changeAttribute) {
           out.print("change ");
-          out.print(((ChangeAttribute) data).id);
+          out.print(changeAttribute.id);
           out.print("\n");
           showText(data, 1);
         } else {
@@ -441,25 +441,25 @@
     out.print(indent);
     out.print(field);
     out.print(':');
-    if (value instanceof String && ((String) value).contains("\n")) {
+    if (value instanceof String string && string.contains("\n")) {
       out.print(' ');
       // Idention for multi-line text is
       // current depth indetion + length of field + length of ": "
       indent = indent(indent.length() + field.length() + spacesDepthRatio);
-      out.print(((String) value).replace("\n", "\n" + indent).trim());
+      out.print(string.replace("\n", "\n" + indent).trim());
       out.print('\n');
-    } else if (value instanceof Long && isDateField(field)) {
+    } else if (value instanceof Long l && isDateField(field)) {
       out.print(' ');
-      out.print(dtf.format(Instant.ofEpochSecond((Long) value)));
+      out.print(dtf.format(Instant.ofEpochSecond(l)));
       out.print('\n');
     } else if (isPrimitive(value)) {
       out.print(' ');
       out.print(value);
       out.print('\n');
-    } else if (value instanceof Collection) {
+    } else if (value instanceof Collection<?> collection) {
       out.print('\n');
       boolean firstElement = true;
-      for (Object thing : ((Collection<?>) value)) {
+      for (Object thing : collection) {
         // The name of the collection was initially printed at the beginning
         // of this routine.  Beginning at the second sub-element, reprint
         // the collection name so humans can separate individual elements
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 2f871c1..428aa74b 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -175,8 +175,7 @@
 
     @Override
     public boolean equals(Object o) {
-      if (o instanceof Range) {
-        Range range = (Range) o;
+      if (o instanceof Range range) {
         return min == range.min && max == range.max;
       }
       return false;
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 9b80512..af2f2fd 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -635,16 +635,11 @@
       input.name = Strings.nullToEmpty(input.name);
       input.email = Strings.nullToEmpty(input.email);
 
-      RefPermission perm;
-      switch (input.type) {
-        case AUTHOR:
-          perm = RefPermission.FORGE_AUTHOR;
-          break;
-        case COMMITTER:
-        default:
-          perm = RefPermission.FORGE_COMMITTER;
-          break;
-      }
+      RefPermission perm =
+          switch (input.type) {
+            case AUTHOR -> RefPermission.FORGE_AUTHOR;
+            case COMMITTER -> RefPermission.FORGE_COMMITTER;
+          };
 
       PersonIdent newIdent =
           new PersonIdent(input.name, input.email, TimeUtil.now(), serverIdent.get().getZoneId());
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index 3f2d0b5..9e95f70 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.BlameInfo;
 import com.google.gerrit.extensions.common.RangeInfo;
@@ -32,6 +34,8 @@
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gitiles.blame.cache.BlameCache;
+import com.google.gitiles.blame.cache.IgnoreRevFileReader;
+import com.google.gitiles.blame.cache.IgnoreRevFileReader.IgnoreRevsFile;
 import com.google.gitiles.blame.cache.Region;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -48,6 +52,7 @@
 import org.kohsuke.args4j.Option;
 
 public class GetBlame implements RestReadView<FileResource> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final BlameCache blameCache;
@@ -139,7 +144,7 @@
     List<Region> blameRegions;
     try (TraceContext.TraceTimer ignored =
         TraceContext.newTimer("blameCache.get", Metadata.empty())) {
-      blameRegions = blameCache.get(repository, id, path);
+      blameRegions = blameCache.get(repository, id, path, getIgnoreIds(revWalk, id));
     }
 
     int from = 1;
@@ -160,6 +165,28 @@
     return result;
   }
 
+  private static ImmutableSet<ObjectId> getIgnoreIds(RevWalk rw, ObjectId id) {
+    IgnoreRevsFile file;
+    try {
+      file = IgnoreRevFileReader.read(rw, id);
+    } catch (IOException e) {
+      logger.atWarning().withCause(e).log(
+          "Cannot read %s at %s", IgnoreRevFileReader.DEFAULT_PATH, id.name());
+      return ImmutableSet.of();
+    }
+    if (file.tooLarge()) {
+      logger.atWarning().log(
+          "%s at %s is larger than %d bytes, not ignoring any revisions",
+          IgnoreRevFileReader.DEFAULT_PATH, id.name(), IgnoreRevFileReader.MAX_SIZE);
+      return ImmutableSet.of();
+    } else if (file.truncated()) {
+      logger.atInfo().log(
+          "Ignoring only the first %d revisions listed in %s at %s",
+          IgnoreRevFileReader.MAX_REVS, IgnoreRevFileReader.DEFAULT_PATH, id.name());
+    }
+    return file.ids();
+  }
+
   private static BlameInfo toBlameInfo(RevCommit commit, PersonIdent sourceAuthor) {
     BlameInfo blameInfo = new BlameInfo();
     blameInfo.author = sourceAuthor.getName();
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 3cc578d..dce4531 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
@@ -74,6 +75,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
 
 public class ReviewersUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -134,6 +136,7 @@
   private final AccountControl.Factory accountControlFactory;
   private final Provider<CurrentUser> self;
   private final ServiceUserClassifier serviceUserClassifier;
+  private final ExecutorService executor;
 
   @Inject
   ReviewersUtil(
@@ -149,7 +152,8 @@
       IndexConfig indexConfig,
       AccountControl.Factory accountControlFactory,
       Provider<CurrentUser> self,
-      ServiceUserClassifier serviceUserClassifier) {
+      ServiceUserClassifier serviceUserClassifier,
+      @FanOutExecutor ExecutorService executor) {
     this.accountVisibility = accountVisibility;
     this.accountLoaderFactory = accountLoaderFactory;
     this.accountQueryBuilder = accountQueryBuilder;
@@ -163,6 +167,7 @@
     this.accountControlFactory = accountControlFactory;
     this.self = self;
     this.serviceUserClassifier = serviceUserClassifier;
+    this.executor = executor;
   }
 
   public interface VisibilityControl {
@@ -219,31 +224,34 @@
     // Filter accounts by visibility, skip service users and enforce limit
     List<Account.Id> filteredRecommendations = new ArrayList<>();
     try (Timer0.Context ctx = metrics.filterVisibility.start()) {
+      List<Account.Id> eligibleCandidates = new ArrayList<>();
       for (Account.Id reviewer : sortedRecommendations) {
-        if (filteredRecommendations.size() >= limit) {
-          logger.atFine().log("Skip results because the limit (%s) has been reached.", limit);
-          break;
-        }
         if (suggestReviewers.isSkipServiceUsers()
             && serviceUserClassifier.isServiceUser(reviewer)) {
           logger.atFine().log("Filter out %s because it's a service user", reviewer);
           continue;
         }
-        // Check if change is visible to reviewer and if the current user can see reviewer
-        if (!visibilityControl.isVisibleTo(reviewer)) {
-          logger.atFine().log("Filter out %s because this user cannot see the change", reviewer);
-          continue;
-        }
+        eligibleCandidates.add(reviewer);
+      }
 
-        // Check if the current user can see reviewer
-        if (!accountControl.canSee(reviewer)) {
-          logger.atFine().log(
-              "Filter out %s because the caller (%s) cannot see the account",
-              reviewer, self.get().getLoggableName());
-          continue;
+      if (!eligibleCandidates.isEmpty() && limit > 0) {
+        VisibilityCheckPipeline pipeline =
+            new VisibilityCheckPipeline(
+                eligibleCandidates,
+                id -> isSuggestable(id, visibilityControl, accountControl, currentUser),
+                executor,
+                limit);
+        try {
+          Account.Id reviewer;
+          while ((reviewer = pipeline.get()) != null) {
+            filteredRecommendations.add(reviewer);
+          }
+        } catch (InterruptedException e) {
+          logger.atWarning().withCause(e).log("Interrupted while checking reviewer visibility");
+          Thread.currentThread().interrupt();
+        } finally {
+          pipeline.stop();
         }
-
-        filteredRecommendations.add(reviewer);
       }
     }
     logger.atFine().log("Filtered recommendations: %s", filteredRecommendations);
@@ -259,6 +267,24 @@
     return suggestedReviewers;
   }
 
+  private static boolean isSuggestable(
+      Account.Id id,
+      VisibilityControl visibilityControl,
+      AccountControl accountControl,
+      CurrentUser currentUser) {
+    if (!visibilityControl.isVisibleTo(id)) {
+      logger.atFine().log("Filter out %s because this user cannot see the change", id);
+      return false;
+    }
+    if (!accountControl.canSee(id)) {
+      logger.atFine().log(
+          "Filter out %s because the caller (%s) cannot see the account",
+          id, currentUser.getLoggableName());
+      return false;
+    }
+    return true;
+  }
+
   private static Account.Id fromIdField(FieldBundle f, boolean useLegacyNumericFields) {
     if (useLegacyNumericFields) {
       return Account.id(f.<Integer>getValue(AccountField.ID_FIELD_SPEC).intValue());
diff --git a/java/com/google/gerrit/server/restapi/change/VisibilityCheckPipeline.java b/java/com/google/gerrit/server/restapi/change/VisibilityCheckPipeline.java
new file mode 100644
index 0000000..26a1673
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/VisibilityCheckPipeline.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Checks reviewer candidates concurrently and returns the visible ones in candidate order.
+ *
+ * <p>At most {@code limit + 2} checks are started up front. Whenever a check finds a candidate to
+ * be not visible, the next unchecked candidate is started, so a single slow check does not hold
+ * back the rest of the window. Results are returned in candidate order, so the ranking of the
+ * candidates is preserved. Processing stops once {@code limit} visible candidates have been
+ * returned.
+ *
+ * <p>{@link #get()} must only be called from a single thread.
+ */
+class VisibilityCheckPipeline {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  /** Decides whether a candidate should be suggested. May block, e.g. on RPCs. */
+  @FunctionalInterface
+  interface CandidateCheck {
+    boolean isVisible(Account.Id id) throws Exception;
+  }
+
+  private record CandidateResult(Account.Id id, boolean visible) {}
+
+  private final ImmutableList<Account.Id> candidates;
+  private final CandidateCheck check;
+  private final Executor executor;
+  private final int limit;
+  private final List<CompletableFuture<CandidateResult>> futures;
+
+  // Guarded by this.
+  private int nextCandidateIndex = 0;
+  private volatile boolean stopped = false;
+
+  // Only accessed by the thread calling get().
+  private boolean started = false;
+  private int currentIndex = 0;
+  private int visibleCount = 0;
+
+  VisibilityCheckPipeline(
+      List<Account.Id> candidates, CandidateCheck check, Executor executor, int limit) {
+    this.candidates = ImmutableList.copyOf(candidates);
+    this.check = check;
+    this.executor = executor;
+    this.limit = limit;
+    this.futures = new ArrayList<>(Collections.nCopies(this.candidates.size(), null));
+  }
+
+  private void start() {
+    if (started || stopped) {
+      return;
+    }
+    started = true;
+    if (limit <= 0) {
+      return;
+    }
+    int initialBatchSize = Math.min(limit + 2, candidates.size());
+    for (int i = 0; i < initialBatchSize; i++) {
+      startCandidate(i);
+    }
+  }
+
+  /**
+   * Returns the next visible candidate in candidate order, or {@code null} if there are no more
+   * visible candidates or the limit has been reached.
+   */
+  @Nullable
+  Account.Id get() throws InterruptedException {
+    start();
+    while (visibleCount < limit && currentIndex < candidates.size()) {
+      CompletableFuture<CandidateResult> future;
+      synchronized (this) {
+        if (stopped) {
+          return null;
+        }
+        startCandidate(currentIndex);
+        future = futures.get(currentIndex);
+      }
+
+      CandidateResult result;
+      try {
+        result = future.get();
+      } catch (ExecutionException e) {
+        logger.atWarning().withCause(e).log(
+            "Failed visibility check execution for candidate %s; skipping",
+            candidates.get(currentIndex));
+        result = new CandidateResult(candidates.get(currentIndex), false);
+      } catch (CancellationException e) {
+        return null;
+      }
+
+      currentIndex++;
+
+      if (result.visible()) {
+        visibleCount++;
+        if (visibleCount >= limit) {
+          logger.atFine().log("Skip results because the limit (%s) has been reached.", limit);
+          stop();
+        }
+        return result.id();
+      }
+    }
+    return null;
+  }
+
+  private synchronized void startCandidate(int index) {
+    if (stopped || index >= candidates.size() || futures.get(index) != null) {
+      return;
+    }
+    // Reserve the slot before submitting the check. With a direct executor the check (and the
+    // startNext() it may trigger) runs synchronously inside execute(), so the slot and
+    // nextCandidateIndex must already be updated to avoid checking the same candidate twice.
+    CompletableFuture<CandidateResult> future = new CompletableFuture<>();
+    futures.set(index, future);
+    if (index >= nextCandidateIndex) {
+      nextCandidateIndex = index + 1;
+    }
+
+    Account.Id id = candidates.get(index);
+    try {
+      executor.execute(
+          () -> {
+            CandidateResult result = runCheck(id);
+            future.complete(result);
+            if (!result.visible()) {
+              startNext();
+            }
+          });
+    } catch (RejectedExecutionException e) {
+      logger.atWarning().withCause(e).log(
+          "Could not schedule visibility check for candidate %s; skipping", id);
+      future.complete(new CandidateResult(id, false));
+    }
+  }
+
+  private CandidateResult runCheck(Account.Id id) {
+    if (stopped) {
+      return new CandidateResult(id, false);
+    }
+    try {
+      return new CandidateResult(id, check.isVisible(id));
+    } catch (Exception e) {
+      logger.atWarning().withCause(e).log("Failed visibility check for candidate %s; skipping", id);
+      return new CandidateResult(id, false);
+    }
+  }
+
+  private synchronized void startNext() {
+    if (stopped || nextCandidateIndex >= candidates.size()) {
+      return;
+    }
+    startCandidate(nextCandidateIndex);
+  }
+
+  /**
+   * Stops the pipeline. No new checks are started, and checks that have not started running yet are
+   * skipped. Checks that are already running are not interrupted.
+   */
+  synchronized void stop() {
+    stopped = true;
+    for (CompletableFuture<CandidateResult> f : futures) {
+      if (f != null && !f.isDone()) {
+        f.cancel(false);
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index 73c762e..df526ca 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -151,8 +151,7 @@
       this.command = task.toString();
       this.queueName = task.getQueueName();
 
-      if (task instanceof ProjectTask) {
-        ProjectTask<?> projectTask = ((ProjectTask<?>) task);
+      if (task instanceof ProjectTask<?> projectTask) {
         Project.NameKey name = projectTask.getProjectNameKey();
         if (name != null) {
           this.projectName = name.get();
diff --git a/java/com/google/gerrit/server/restapi/config/TasksCollection.java b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
index 0e220d1..8a83bc9 100644
--- a/java/com/google/gerrit/server/restapi/config/TasksCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
@@ -88,8 +88,8 @@
     }
 
     Task<?> task = workQueue.getTask(taskId);
-    if (task instanceof ProjectTask) {
-      Project.NameKey nameKey = ((ProjectTask<?>) task).getProjectNameKey();
+    if (task instanceof ProjectTask<?> projectTask) {
+      Project.NameKey nameKey = projectTask.getProjectNameKey();
       if (nameKey != null) {
         Optional<ProjectState> state = projectCache.get(nameKey);
         if (!state.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index 4edbc95..0019018 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -39,8 +39,8 @@
 public class GroupJson {
   public static GroupOptionsInfo createOptions(GroupDescription.Basic group) {
     GroupOptionsInfo options = new GroupOptionsInfo();
-    if (group instanceof GroupDescription.Internal
-        && ((GroupDescription.Internal) group).isVisibleToAll()) {
+    if (group instanceof GroupDescription.Internal internalGroup
+        && internalGroup.isVisibleToAll()) {
       options.visibleToAll = true;
     }
     return options;
@@ -91,8 +91,8 @@
       throws PermissionBackendException {
     GroupInfo info = createBasicGroupInfo(group);
 
-    if (group instanceof GroupDescription.Internal) {
-      addInternalDetails(info, (GroupDescription.Internal) group, groupControlSupplier);
+    if (group instanceof GroupDescription.Internal internalGroup) {
+      addInternalDetails(info, internalGroup, groupControlSupplier);
     }
 
     return info;
diff --git a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
index 6b46164..29b7e68 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -127,10 +127,10 @@
       }
 
       Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy);
-      if (m instanceof ResolveMerger && useGitattributesForMerge) {
+      if (m instanceof ResolveMerger resolveMerger && useGitattributesForMerge) {
         // We need to set the attributes provider before attempting the merge in order to read and
         // honor gitattributes merge settings correctly
-        ((ResolveMerger) m).setAttributesNodeProvider(git.createAttributesNodeProvider());
+        resolveMerger.setAttributesNodeProvider(git.createAttributesNodeProvider());
       }
       if (m.merge(false, targetCommit, sourceCommit)) {
         result.mergeable = true;
@@ -138,8 +138,8 @@
         result.contentMerged = m.getResultTreeId().equals(targetCommit.getTree());
       } else {
         result.mergeable = false;
-        if (m instanceof ResolveMerger) {
-          result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
+        if (m instanceof ResolveMerger resolveMerger) {
+          result.conflicts = resolveMerger.getUnmergedPaths();
         }
       }
     } catch (InvalidMergeStrategyException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index c2060e5..b34122e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -262,25 +262,21 @@
   }
 
   private void appendAndLogErrorMessage(StringBuilder errorMessages, ReceiveCommand cmd) {
-    String msg;
-    switch (cmd.getResult()) {
-      case REJECTED_CURRENT_BRANCH:
-        msg = format("Cannot delete %s: it is the current branch", cmd.getRefName());
-        break;
-      case REJECTED_OTHER_REASON:
-        msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
-        break;
-      case LOCK_FAILURE:
-      case NOT_ATTEMPTED:
-      case OK:
-      case REJECTED_MISSING_OBJECT:
-      case REJECTED_NOCREATE:
-      case REJECTED_NODELETE:
-      case REJECTED_NONFASTFORWARD:
-      default:
-        msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
-        break;
-    }
+    String msg =
+        switch (cmd.getResult()) {
+          case REJECTED_CURRENT_BRANCH ->
+              format("Cannot delete %s: it is the current branch", cmd.getRefName());
+          case REJECTED_OTHER_REASON ->
+              format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
+          case NOT_ATTEMPTED,
+              REJECTED_NOCREATE,
+              REJECTED_NODELETE,
+              REJECTED_NONFASTFORWARD,
+              REJECTED_MISSING_OBJECT,
+              LOCK_FAILURE,
+              OK ->
+              format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
+        };
     logger.atSevere().log("%s", msg);
     errorMessages.append(msg);
     errorMessages.append("\n");
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 41c64ca..192e499 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -218,9 +218,8 @@
     }
 
     ImmutableList<WebLinkInfo> webLinks = links.getTagLinks(projectState.getName(), ref.getName());
-    if (object instanceof RevTag) {
+    if (object instanceof RevTag tag) {
       // Annotated or signed tag
-      RevTag tag = (RevTag) object;
       PersonIdent tagger = tag.getTaggerIdent();
       return new TagInfo(
           ref.getName(),
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index 8411f68..43b44d5 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.NeedsParams;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollection;
@@ -134,8 +133,7 @@
   }
 
   @Nullable
-  private ProjectResource _parse(String id, boolean checkAccess)
-      throws PermissionBackendException, ResourceConflictException {
+  private ProjectResource _parse(String id, boolean checkAccess) throws PermissionBackendException {
     try {
       ProjectUtil.validateProjectName(id);
     } catch (InvalidProjectNameException e) {
@@ -157,15 +155,14 @@
       // WRITE_CONFIG is checked here because it's only allowed to project owners (ACCESS may also
       // be allowed for other users). Allowing project owners to access here will help them to view
       // and update the config of hidden projects easily.
-      if (state.get().statePermitsRead()) {
-        if (!permissionBackend.currentUser().project(nameKey).test(ProjectPermission.ACCESS)) {
-          return null;
-        }
-      } else if (!permissionBackend
-          .currentUser()
-          .project(nameKey)
-          .test(ProjectPermission.WRITE_CONFIG)) {
-        state.get().checkStatePermitsRead();
+      // For everyone else a hidden project must be indistinguishable from a non-existing one, so
+      // that its existence and state are not disclosed.
+      ProjectPermission requiredPermission =
+          state.get().statePermitsRead()
+              ? ProjectPermission.ACCESS
+              : ProjectPermission.WRITE_CONFIG;
+      if (!permissionBackend.currentUser().project(nameKey).test(requiredPermission)) {
+        return null;
       }
     }
     return new ProjectResource(state.get(), user.get());
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/prolog/PrologRuleEvaluator.java
index 3033dd7..a32335d 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologRuleEvaluator.java
@@ -442,8 +442,7 @@
       List<Term> r;
       if (resultsTerm instanceof ListTerm) {
         r = new ArrayList<>();
-        for (Term t = resultsTerm; t instanceof ListTerm; ) {
-          ListTerm l = (ListTerm) t;
+        for (Term t = resultsTerm; t instanceof ListTerm l; ) {
           r.add(l.car().dereference());
           t = l.cdr().dereference();
         }
diff --git a/java/com/google/gerrit/server/rules/prolog/RulesCache.java b/java/com/google/gerrit/server/rules/prolog/RulesCache.java
index 00f25ae..88fa45f 100644
--- a/java/com/google/gerrit/server/rules/prolog/RulesCache.java
+++ b/java/com/google/gerrit/server/rules/prolog/RulesCache.java
@@ -207,8 +207,8 @@
       Term m = e.getMessageTerm();
       if (m instanceof StructureTerm && "syntax_error".equals(m.name()) && m.arity() >= 1) {
         StringBuilder msg = new StringBuilder();
-        if (m.arg(0) instanceof ListTerm) {
-          msg.append(Joiner.on(' ').join(((ListTerm) m.arg(0)).toJava()));
+        if (m.arg(0) instanceof ListTerm lt) {
+          msg.append(Joiner.on(' ').join(lt.toJava()));
         } else {
           msg.append(m.arg(0).toString());
         }
@@ -231,8 +231,7 @@
   private static String prettyProlog(Term at) {
     StringBuilder b = new StringBuilder();
     for (Object o : ((ListTerm) at).toJava()) {
-      if (o instanceof Term) {
-        Term t = (Term) o;
+      if (o instanceof Term t) {
         if (!(t instanceof StructureTerm)) {
           b.append(t.toString()).append(' ');
           continue;
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 0a458d7..0ea2e5c 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -218,6 +218,7 @@
       GroupReference defaultUsersGroup) {
     grant(config, heads, codeReviewLabel, -1, 1, defaultUsersGroup);
     grant(config, heads, Permission.FORGE_AUTHOR, defaultUsersGroup);
+    grant(config, heads, Permission.AI_REVIEW, registered);
 
     config.upsertAccessSection(
         "refs/for/*",
@@ -226,10 +227,6 @@
           grant(config, refsFor, Permission.PUSH, defaultUsersGroup);
           grant(config, refsFor, Permission.PUSH_MERGE, defaultUsersGroup);
         });
-
-    config.upsertAccessSection(
-        AccessSection.ALL,
-        all -> grant(config, all, Permission.POST_REVIEW_COMMENT, defaultUsersGroup));
   }
 
   private void initDefaultAclsForServiceUsers(
diff --git a/java/com/google/gerrit/server/schema/GrantPostReviewCommentPermission.java b/java/com/google/gerrit/server/schema/GrantAiReviewPermission.java
similarity index 65%
rename from java/com/google/gerrit/server/schema/GrantPostReviewCommentPermission.java
rename to java/com/google/gerrit/server/schema/GrantAiReviewPermission.java
index e9f34fd..285e6f2 100644
--- a/java/com/google/gerrit/server/schema/GrantPostReviewCommentPermission.java
+++ b/java/com/google/gerrit/server/schema/GrantAiReviewPermission.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2025 The Android Open Source Project
+// Copyright (C) 2026 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -29,17 +29,19 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
 /**
- * This class adds the "postReviewComment" permission to all hosts that call this method with the
- * relevant projectName. This class should be called with AllProjects as the project, by all hosts
- * before enabling the "postReviewComment" permission.
+ * Adds an explicit {@code allow aiReview group Registered Users} rule on {@code refs/heads/*} to
+ * {@code All-Projects} so existing sites preserve their previous default-allow behavior after
+ * {@code aiReview} switches to standard default-deny semantics.
+ *
+ * <p>Skipped when an {@code aiReview} permission already exists on {@code refs/heads/*} so admin
+ * customizations are not overwritten.
  */
-public class GrantPostReviewCommentPermission {
+public class GrantAiReviewPermission {
 
   private final GitRepositoryManager repoManager;
   private final ProjectConfig.Factory projectConfigFactory;
@@ -47,7 +49,7 @@
   private final PersonIdent serverUser;
 
   @Inject
-  public GrantPostReviewCommentPermission(
+  public GrantAiReviewPermission(
       GitRepositoryManager repoManager,
       ProjectConfig.Factory projectConfigFactory,
       SystemGroupBackend systemGroupBackend,
@@ -64,32 +66,23 @@
       MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, repo);
       ProjectConfig projectConfig = projectConfigFactory.read(md);
 
-      AtomicBoolean shouldExit = new AtomicBoolean(false);
-      projectConfig.upsertAccessSection(
-          AccessSection.ALL,
-          all -> {
-            Permission permissionOnRefsStar =
-                all.build().getPermission(Permission.POST_REVIEW_COMMENT);
-            if (permissionOnRefsStar != null) {
-              if (permissionOnRefsStar.getRule(registeredUsers) == null) {
-                // If admins already changed the permission, don't do anything.
-                shouldExit.set(true);
-                return;
-              }
-              // permission already exists on refs/* for Registered Users, don't do anything.
-              return;
-            }
-            // If the permission doesn't exist on refs/* for Registered Users, grant it.
-            grant(projectConfig, all, Permission.POST_REVIEW_COMMENT, registeredUsers);
-          });
-
-      if (shouldExit.get()) {
+      boolean alreadyHasAiReview =
+          projectConfig.getAccessSection(AccessSection.HEADS) != null
+              && projectConfig
+                      .getAccessSection(AccessSection.HEADS)
+                      .getPermission(Permission.AI_REVIEW)
+                  != null;
+      if (alreadyHasAiReview) {
         return;
       }
 
+      projectConfig.upsertAccessSection(
+          AccessSection.HEADS,
+          heads -> grant(projectConfig, heads, Permission.AI_REVIEW, registeredUsers));
+
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
-      md.setMessage("Add Post Review Comment permission for all registered users\n");
+      md.setMessage("Add AI Review permission for all registered users\n");
 
       projectConfig.commit(md);
     }
diff --git a/java/com/google/gerrit/server/schema/H2CustomLockAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/H2CustomLockAccountPatchReviewStore.java
index 4e44a27..1068d17 100644
--- a/java/com/google/gerrit/server/schema/H2CustomLockAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/H2CustomLockAccountPatchReviewStore.java
@@ -129,6 +129,7 @@
               wait(waitMs);
               backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
             } catch (InterruptedException | RuntimeException e) {
+              waiting.remove(thread);
               unlock(thread);
               throw e;
             }
@@ -142,7 +143,11 @@
       }
 
       private void unlock(Thread thread) {
-        acquired.remove(thread);
+        if (!acquired.remove(thread)) {
+          // Not a holder (a waiter that gave up)
+          notifyAll();
+          return;
+        }
         try {
           if (acquired.isEmpty()) {
             raw.unlock();
diff --git a/java/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStore.java
index c66b3b4..5975474 100644
--- a/java/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStore.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.H2Url;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.File;
@@ -41,7 +42,6 @@
 @Singleton
 public class H2JGitLockAccountPatchReviewStore extends H2CustomLockAccountPatchReviewStore {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final String H2_DB_URL_PREFIX = "jdbc:h2:file:";
   private final File lockTarget;
 
   @Inject
@@ -52,13 +52,15 @@
 
   @VisibleForTesting
   static File lockTargetFromUrl(String h2Url) {
-    if (!h2Url.startsWith(H2_DB_URL_PREFIX)) {
+    String path;
+    if (h2Url.startsWith(H2Url.ASYNC_PREFIX)) {
+      path = h2Url.substring(H2Url.ASYNC_PREFIX.length());
+    } else if (h2Url.startsWith(H2Url.FILE_PREFIX)) {
+      path = h2Url.substring(H2Url.FILE_PREFIX.length());
+    } else {
       throw new IllegalArgumentException("Not a valid H2 file URL: " + h2Url);
     }
 
-    // URL format: "jdbc:h2:file:/path/to/db" - where ";" in the path is escaped as "\;"
-    String path = h2Url.substring(H2_DB_URL_PREFIX.length());
-
     // Split on first unescaped ";" to drop options, then unescape "\;" in the path
     return new File(
         Iterables.get(Splitter.on(Pattern.compile("(?<!\\\\);")).split(path), 0)
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index fa53fe7..102d1ec 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.util.H2Url;
 import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -84,15 +85,11 @@
       if (url == null || url.contains(H2_DB)) {
         String lockType = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, H2_LOCK_TYPE);
         switch (lockType != null ? lockType : "") {
-          case "":
-            impl = H2AccountPatchReviewStore.class;
-            break;
-          case H2_LOCK_TYPE_JGIT:
-            impl = H2JGitLockAccountPatchReviewStore.class;
-            break;
-          default:
-            throw new IllegalArgumentException(
-                "Invalid accountPatchReviewDb.h2LockType value: " + lockType);
+          case "" -> impl = H2AccountPatchReviewStore.class;
+          case H2_LOCK_TYPE_JGIT -> impl = H2JGitLockAccountPatchReviewStore.class;
+          default ->
+              throw new IllegalArgumentException(
+                  "Invalid accountPatchReviewDb.h2LockType value: " + lockType);
         }
       } else if (url.contains(POSTGRESQL)) {
         impl = PostgresqlAccountPatchReviewStore.class;
@@ -118,15 +115,13 @@
     String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     if (url == null || url.contains(H2_DB)) {
       String lockType = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, H2_LOCK_TYPE);
-      switch (lockType != null ? lockType : "") {
-        case "":
-          return new H2AccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
-        case H2_LOCK_TYPE_JGIT:
-          return new H2JGitLockAccountPatchReviewStore(cfg, sitePaths);
-        default:
-          throw new IllegalArgumentException(
-              "Invalid accountPatchReviewDb.h2LockType value: " + lockType);
-      }
+      return switch (lockType != null ? lockType : "") {
+        case "" -> new H2AccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
+        case H2_LOCK_TYPE_JGIT -> new H2JGitLockAccountPatchReviewStore(cfg, sitePaths);
+        default ->
+            throw new IllegalArgumentException(
+                "Invalid accountPatchReviewDb.h2LockType value: " + lockType);
+      };
     }
     if (url.contains(POSTGRESQL)) {
       return new PostgresqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
@@ -159,6 +154,15 @@
     if (url == null) {
       return createH2Url(sitePaths.db_dir.resolve("account_patch_reviews"));
     }
+    url = normalizeH2Url(url);
+    return url;
+  }
+
+  @VisibleForTesting
+  static String normalizeH2Url(String url) {
+    if (url.startsWith(H2Url.FILE_PREFIX)) {
+      return H2Url.ASYNC_PREFIX + url.substring(H2Url.FILE_PREFIX.length());
+    }
     return url;
   }
 
@@ -461,7 +465,7 @@
   @VisibleForTesting
   static String createH2Url(Path path) {
     return new StringBuilder()
-        .append("jdbc:h2:file:")
+        .append(H2Url.ASYNC_PREFIX)
         .append(path.toAbsolutePath().toString())
         .toString()
         .replace(";", "\\;");
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
index 69e6fb3..05814e6 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
@@ -35,7 +35,8 @@
               Schema_183.class,
               Schema_184.class,
               Schema_185.class,
-              Schema_186.class)
+              Schema_186.class,
+              Schema_187.class)
           .collect(toImmutableSortedMap(naturalOrder(), v -> guessVersion(v).get(), v -> v));
 
   public static final int FIRST = ALL.firstKey();
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/SchemaModule.java
index 1106108..60e86e0 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.config.GerritImportedServerIdsProvider;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.git.AllUsersRepositoryProvider;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.notedb.RepoSequence.DisabledGitRefUpdatedRepoGroupsSequenceProvider;
 import com.google.gerrit.server.query.change.ChangeNumberBitmapMaskAlgorithm;
@@ -58,6 +59,7 @@
     bind(AllProjectsName.class).toProvider(AllProjectsNameProvider.class).in(SINGLETON);
 
     bind(AllUsersName.class).toProvider(AllUsersNameProvider.class).in(SINGLETON);
+    install(new AllUsersRepositoryProvider.Module());
 
     bind(String.class)
         .annotatedWith(AnonymousCowardName.class)
diff --git a/java/com/google/gerrit/server/schema/Schema_186.java b/java/com/google/gerrit/server/schema/Schema_186.java
index de1e315..7b68fb0 100644
--- a/java/com/google/gerrit/server/schema/Schema_186.java
+++ b/java/com/google/gerrit/server/schema/Schema_186.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
+/** Retains schema version 186 after reverting the Post Review Comment migration. */
 public class Schema_186 implements NoteDbSchemaVersion {
   @Override
-  public void upgrade(NoteDbSchemaVersion.Arguments args, UpdateUI ui) throws Exception {
-    new GrantPostReviewCommentPermission(
-            args.repoManager, args.projectConfigFactory, args.systemGroupBackend, args.serverUser)
-        .execute(args.allProjects);
-  }
+  public void upgrade(NoteDbSchemaVersion.Arguments args, UpdateUI ui) throws Exception {}
 }
diff --git a/java/com/google/gerrit/server/schema/Schema_187.java b/java/com/google/gerrit/server/schema/Schema_187.java
new file mode 100644
index 0000000..6b2eb7b
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_187.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+public class Schema_187 implements NoteDbSchemaVersion {
+  @Override
+  public void upgrade(NoteDbSchemaVersion.Arguments args, UpdateUI ui) throws Exception {
+    new GrantAiReviewPermission(
+            args.repoManager, args.projectConfigFactory, args.systemGroupBackend, args.serverUser)
+        .execute(args.allProjects);
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
index 188649f..52c1267 100644
--- a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -56,7 +56,6 @@
     return ImmutableList.of(
         "[access \"refs/*\"]",
         "  read = group Administrators",
-        "  postReviewComment = group " + usersGroupName,
         "  read = block group Blocked Users",
         "[access \"refs/for/*\"]",
         "  addPatchSet = group " + usersGroupName,
@@ -70,6 +69,7 @@
         "  create = group Project Owners",
         "  editTopicName = +force group Administrators",
         "  editTopicName = +force group Project Owners",
+        "  aiReview = group Registered Users",
         "  forgeAuthor = group " + usersGroupName,
         "  forgeCommitter = group Administrators",
         "  forgeCommitter = group Project Owners",
diff --git a/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
index c38df6b..aeacc2e 100644
--- a/java/com/google/gerrit/server/ssh/SshAddressesModule.java
+++ b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -82,8 +82,7 @@
     List<InetSocketAddress> local = new ArrayList<>();
 
     for (SocketAddress addr : listen) {
-      if (addr instanceof InetSocketAddress) {
-        InetSocketAddress inetAddr = (InetSocketAddress) addr;
+      if (addr instanceof InetSocketAddress inetAddr) {
         if (inetAddr.getAddress().isLoopbackAddress()) {
           local.add(inetAddr);
         } else {
diff --git a/java/com/google/gerrit/server/submit/MergeMetrics.java b/java/com/google/gerrit/server/submit/MergeMetrics.java
index ea9e678..201ce5d 100644
--- a/java/com/google/gerrit/server/submit/MergeMetrics.java
+++ b/java/com/google/gerrit/server/submit/MergeMetrics.java
@@ -153,16 +153,14 @@
       // the uploader are ignored, we must check if there is any MagicLabelPredicate for the
       // Code-Review label that ignores approvals of the uploader (aka has user set to non_uploader
       // or non_contributor).
-      if (predicate instanceof MagicLabelPredicates.PostFilterMagicLabelPredicate) {
-        MagicLabelPredicates.PostFilterMagicLabelPredicate magicLabelPredicate =
-            (MagicLabelPredicates.PostFilterMagicLabelPredicate) predicate;
+      if (predicate
+          instanceof MagicLabelPredicates.PostFilterMagicLabelPredicate magicLabelPredicate) {
         if (magicLabelPredicate.getLabel().equalsIgnoreCase("Code-Review")
             && magicLabelPredicate.ignoresUploaderApprovals()) {
           return true;
         }
-      } else if (predicate instanceof MagicLabelPredicates.IndexMagicLabelPredicate) {
-        MagicLabelPredicates.IndexMagicLabelPredicate magicLabelPredicate =
-            (MagicLabelPredicates.IndexMagicLabelPredicate) predicate;
+      } else if (predicate
+          instanceof MagicLabelPredicates.IndexMagicLabelPredicate magicLabelPredicate) {
         if (magicLabelPredicate.getLabel().equalsIgnoreCase("Code-Review")
             && magicLabelPredicate.ignoresUploaderApprovals()) {
           return true;
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 3c0cd00..3b413f2 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -1086,8 +1086,8 @@
         //
         // If you happen across one of these, the correct fix is to convert the
         // inner IntegrationConflictException to a ResourceConflictException.
-        if (e.getCause() instanceof IntegrationConflictException) {
-          throw (IntegrationConflictException) e.getCause();
+        if (e.getCause() instanceof IntegrationConflictException ice) {
+          throw ice;
         }
         throw new MergeUpdateException(genericMergeError(cs), e);
       }
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index a3bb58b..7e93596 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -90,8 +90,8 @@
       if (obj instanceof RevTag) {
         obj = rw.peel(obj);
       }
-      if (obj instanceof RevCommit) {
-        out.add((RevCommit) obj);
+      if (obj instanceof RevCommit revCommit) {
+        out.add(revCommit);
       }
     }
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
index f133c74..75935cf 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
@@ -44,8 +44,8 @@
       SubmitInput input, Collection<SubmitStrategy> strategies, CommitStatus commitStatus) {
     this.strategies = strategies;
     this.commitStatus = commitStatus;
-    if (input instanceof TestSubmitInput) {
-      failAfterRefUpdates = ((TestSubmitInput) input).failAfterRefUpdates;
+    if (input instanceof TestSubmitInput testSubmitInput) {
+      failAfterRefUpdates = testSubmitInput.failAfterRefUpdates;
     } else {
       failAfterRefUpdates = false;
     }
diff --git a/java/com/google/gerrit/server/util/H2Url.java b/java/com/google/gerrit/server/util/H2Url.java
new file mode 100644
index 0000000..575ca9c
--- /dev/null
+++ b/java/com/google/gerrit/server/util/H2Url.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+public final class H2Url {
+  public static final String ASYNC_PREFIX = "jdbc:h2:async:";
+  public static final String FILE_PREFIX = "jdbc:h2:file:";
+
+  private H2Url() {}
+}
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 2f73622..1ee61f5 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -109,7 +109,7 @@
   public final Runnable wrap(Runnable runnable) {
     final Callable<Object> wrapped = wrap(Executors.callable(runnable));
 
-    if (runnable instanceof ProjectRunnable) {
+    if (runnable instanceof ProjectRunnable projectRunnable) {
       return new ProjectRunnable() {
         @Override
         public void run() {
@@ -123,17 +123,17 @@
 
         @Override
         public Project.NameKey getProjectNameKey() {
-          return ((ProjectRunnable) runnable).getProjectNameKey();
+          return projectRunnable.getProjectNameKey();
         }
 
         @Override
         public String getRemoteName() {
-          return ((ProjectRunnable) runnable).getRemoteName();
+          return projectRunnable.getRemoteName();
         }
 
         @Override
         public boolean hasCustomizedPrint() {
-          return ((ProjectRunnable) runnable).hasCustomizedPrint();
+          return projectRunnable.hasCustomizedPrint();
         }
 
         @Override
diff --git a/java/com/google/gerrit/server/util/SocketUtil.java b/java/com/google/gerrit/server/util/SocketUtil.java
index e617650..006d9ac 100644
--- a/java/com/google/gerrit/server/util/SocketUtil.java
+++ b/java/com/google/gerrit/server/util/SocketUtil.java
@@ -39,8 +39,8 @@
 
   /** Format an address string into {@code host:port} or {@code *:port} syntax. */
   public static String format(SocketAddress s, int defaultPort) {
-    if (s instanceof InetSocketAddress) {
-      final InetSocketAddress addr = (InetSocketAddress) s;
+    if (s instanceof InetSocketAddress inetSocketAddress) {
+      final InetSocketAddress addr = inetSocketAddress;
       if (addr.getPort() == defaultPort) {
         return safeHostname(hostname(addr));
       }
diff --git a/java/com/google/gerrit/sshd/AliasCommand.java b/java/com/google/gerrit/sshd/AliasCommand.java
index 17ea463..d29211fc 100644
--- a/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/java/com/google/gerrit/sshd/AliasCommand.java
@@ -84,8 +84,7 @@
 
     Command cmd = p.getProvider().get();
     checkRequiresCapability(cmd);
-    if (cmd instanceof BaseCommand) {
-      BaseCommand bc = (BaseCommand) cmd;
+    if (cmd instanceof BaseCommand bc) {
       bc.setName(getName());
       bc.setArguments(getArguments());
     }
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index d0204b1..37a8a34 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -385,8 +385,8 @@
       logCauseIfRelevant(e, m);
     }
 
-    if (e instanceof Failure) {
-      final Failure f = (Failure) e;
+    if (e instanceof Failure failure) {
+      final Failure f = failure;
       try {
         err.write((f.getMessage() + "\n").getBytes(ENC));
         err.flush();
@@ -522,10 +522,10 @@
           thisThread.setName("SSH " + taskName);
 
           try {
-            if (thunk instanceof ProjectCommandRunnable) {
+            if (thunk instanceof ProjectCommandRunnable projectCommandRunnable) {
               try (DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans)) {
-                ((ProjectCommandRunnable) thunk).executeParseCommand(pluginOptions);
-                projectName = ((ProjectCommandRunnable) thunk).getProjectName();
+                projectCommandRunnable.executeParseCommand(pluginOptions);
+                projectName = projectCommandRunnable.getProjectName();
                 thunk.run();
               }
             } else {
diff --git a/java/com/google/gerrit/sshd/Commands.java b/java/com/google/gerrit/sshd/Commands.java
index c1edcd8..3835738 100644
--- a/java/com/google/gerrit/sshd/Commands.java
+++ b/java/com/google/gerrit/sshd/Commands.java
@@ -64,16 +64,16 @@
 
   /** Return the name of this command, possibly including any parents. */
   public static String nameOf(CommandName name) {
-    if (name instanceof NestedCommandNameImpl) {
-      return nameOf(((NestedCommandNameImpl) name).parent) + " " + name.value();
+    if (name instanceof NestedCommandNameImpl nestedCommandNameImpl) {
+      return nameOf(nestedCommandNameImpl.parent) + " " + name.value();
     }
     return name.value();
   }
 
   /** Is the second command a direct child of the first command? */
   public static boolean isChild(CommandName parent, CommandName name) {
-    if (name instanceof NestedCommandNameImpl) {
-      return parent.equals(((NestedCommandNameImpl) name).parent);
+    if (name instanceof NestedCommandNameImpl nestedCommandNameImpl) {
+      return parent.equals(nestedCommandNameImpl.parent);
     }
     if (identity().equivalent(parent, CMD_ROOT)) {
       return true;
@@ -83,8 +83,8 @@
 
   @Nullable
   static CommandName parentOf(CommandName name) {
-    if (name instanceof NestedCommandNameImpl) {
-      return ((NestedCommandNameImpl) name).parent;
+    if (name instanceof NestedCommandNameImpl nestedCommandNameImpl) {
+      return nestedCommandNameImpl.parent;
     }
     return null;
   }
diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java
index 54171a3..8e31b01 100644
--- a/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -93,8 +93,8 @@
       final Command cmd = p.getProvider().get();
       checkRequiresCapability(cmd);
       String actualCommandName = commandName;
-      if (cmd instanceof BaseCommand) {
-        final BaseCommand bc = (BaseCommand) cmd;
+      if (cmd instanceof BaseCommand baseCommand) {
+        final BaseCommand bc = baseCommand;
         if (!getName().isEmpty()) {
           actualCommandName = getName() + " " + commandName;
         }
@@ -132,8 +132,8 @@
 
   private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
     String pluginName = null;
-    if (cmd instanceof BaseCommand) {
-      pluginName = ((BaseCommand) cmd).getPluginName();
+    if (cmd instanceof BaseCommand baseCommand) {
+      pluginName = baseCommand.getPluginName();
     }
     try {
       permissionBackend
diff --git a/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index acf2df9..c530854 100644
--- a/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -76,12 +76,11 @@
     ConcurrentMap<String, CommandProvider> m = Maps.newConcurrentMap();
     for (Binding<?> b : allCommands()) {
       final Annotation annotation = b.getKey().getAnnotation();
-      if (annotation instanceof CommandName) {
-        final CommandName n = (CommandName) annotation;
+      if (annotation instanceof CommandName commandName) {
+        final CommandName n = commandName;
         if (!Commands.CMD_ROOT.equals(n) && Commands.isChild(parent, n)) {
           String descr = null;
-          if (annotation instanceof Commands.NestedCommandNameImpl) {
-            Commands.NestedCommandNameImpl impl = ((Commands.NestedCommandNameImpl) annotation);
+          if (annotation instanceof Commands.NestedCommandNameImpl impl) {
             descr = impl.descr();
           }
           m.put(n.value(), new CommandProvider((Provider<Command>) b.getProvider(), descr));
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index 20d7874..b4f2cc0 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -261,10 +261,9 @@
           protected ServerSessionImpl createSession(IoSession io) throws Exception {
             connected.incrementAndGet();
             sessionsCreated.increment();
-            if (io instanceof MinaSession) {
-              if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) {
-                ((SocketSessionConfig) ((MinaSession) io).getSession().getConfig())
-                    .setKeepAlive(keepAlive);
+            if (io instanceof MinaSession minaSession) {
+              if (minaSession.getSession().getConfig() instanceof SocketSessionConfig sc) {
+                sc.setKeepAlive(keepAlive);
               }
             }
 
@@ -402,8 +401,8 @@
     }
 
     IoServiceFactory serviceFactory = getIoServiceFactory();
-    if (serviceFactory instanceof AbstractIoServiceFactory) {
-      shutdownServiceFactoryExecutor((AbstractIoServiceFactory) serviceFactory);
+    if (serviceFactory instanceof AbstractIoServiceFactory abstractIoServiceFactory) {
+      shutdownServiceFactoryExecutor(abstractIoServiceFactory);
     }
   }
 
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 83db146..b43a5b5 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -188,24 +188,13 @@
     event.setProperty(P_USER_CPU, ctx.getUserCpu() + "ms");
     event.setProperty(P_MEMORY, String.valueOf(ctx.getAllocatedMemory()));
 
-    final String status;
-    switch (exitValue) {
-      case BaseCommand.STATUS_CANCEL:
-        status = "killed";
-        break;
-
-      case BaseCommand.STATUS_NOT_FOUND:
-        status = "not-found";
-        break;
-
-      case BaseCommand.STATUS_NOT_ADMIN:
-        status = "not-admin";
-        break;
-
-      default:
-        status = String.valueOf(exitValue);
-        break;
-    }
+    final String status =
+        switch (exitValue) {
+          case BaseCommand.STATUS_CANCEL -> "killed";
+          case BaseCommand.STATUS_NOT_FOUND -> "not-found";
+          case BaseCommand.STATUS_NOT_ADMIN -> "not-admin";
+          default -> String.valueOf(exitValue);
+        };
     event.setProperty(P_STATUS, status);
     String peerAgent = sshSession.getPeerAgent();
     if (peerAgent != null) {
diff --git a/java/com/google/gerrit/sshd/SshSession.java b/java/com/google/gerrit/sshd/SshSession.java
index 90d11c7..9e0e328 100644
--- a/java/com/google/gerrit/sshd/SshSession.java
+++ b/java/com/google/gerrit/sshd/SshSession.java
@@ -122,8 +122,8 @@
   }
 
   private static String format(SocketAddress remote) {
-    if (remote instanceof InetSocketAddress) {
-      final InetSocketAddress sa = (InetSocketAddress) remote;
+    if (remote instanceof InetSocketAddress inetSocketAddress) {
+      final InetSocketAddress sa = inetSocketAddress;
 
       final InetAddress in = sa.getAddress();
       if (in != null) {
diff --git a/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
index 98626de..511339a 100644
--- a/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
+++ b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.DelegateRepository;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectState;
@@ -29,6 +30,7 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
@@ -36,11 +38,13 @@
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
     name = "convert-ref-storage",
-    description = "Convert ref storage to reftable (experimental)",
+    description = "Convert ref storage to reftable",
     runsAt = MASTER_OR_SLAVE)
 public class ConvertRefStorage extends SshCommand {
   @Inject private GitRepositoryManager repoManager;
 
+  @Inject @GerritServerConfig private Config cfg;
+
   private enum StorageFormatOption {
     reftable,
     refdir,
@@ -74,10 +78,15 @@
   @Override
   public void run() throws Exception {
     enableGracefulStop();
+    // reftable is unservable with core.usePerRequestRefCache; refuse converting to it.
+    if (storageFormat == StorageFormatOption.reftable
+        && cfg.getBoolean("core", null, "usePerRequestRefCache", true)) {
+      throw die("Converting to reftable requires core.usePerRequestRefCache=false");
+    }
     Project.NameKey projectName = projectState.getNameKey();
     try (Repository repo = repoManager.openRepository(projectName)) {
-      if (repo instanceof DelegateRepository) {
-        ((DelegateRepository) repo).convertRefStorage(storageFormat.name(), writeLogs, backup);
+      if (repo instanceof DelegateRepository delegateRepository) {
+        delegateRepository.convertRefStorage(storageFormat.name(), writeLogs, backup);
       } else {
         checkState(
             repo instanceof FileRepository, "Repository is not an instance of FileRepository!");
diff --git a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 63a01d00..f48ddce 100644
--- a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -91,27 +91,17 @@
             .run(projectNames, aggressive, showProgress ? stdout : null);
     if (result.hasErrors()) {
       for (GcError e : result.getErrors()) {
-        String msg;
-        switch (e.getType()) {
-          case REPOSITORY_NOT_FOUND:
-            msg = "error: project \"" + e.getProjectName() + "\" not found";
-            break;
-          case GC_ALREADY_SCHEDULED:
-            msg =
-                "error: garbage collection for project \""
-                    + e.getProjectName()
-                    + "\" was already scheduled";
-            break;
-          case GC_FAILED:
-            msg = "error: garbage collection for project \"" + e.getProjectName() + "\" failed";
-            break;
-          default:
-            msg =
-                "error: garbage collection for project \""
-                    + e.getProjectName()
-                    + "\" failed: "
-                    + e.getType();
-        }
+        String msg =
+            switch (e.getType()) {
+              case REPOSITORY_NOT_FOUND ->
+                  "error: project \"" + e.getProjectName() + "\" not found";
+              case GC_ALREADY_SCHEDULED ->
+                  "error: garbage collection for project \""
+                      + e.getProjectName()
+                      + "\" was already scheduled";
+              case GC_FAILED ->
+                  "error: garbage collection for project \"" + e.getProjectName() + "\" failed";
+            };
         stdout.print(msg + "\n");
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java b/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java
index e9058b4..e33ba47 100644
--- a/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java
@@ -33,20 +33,13 @@
 
   @Override
   public void run() throws Exception {
-    int current;
-    switch (name) {
-      case "changes":
-        current = sequences.currentChangeId();
-        break;
-      case "accounts":
-        current = sequences.currentAccountId();
-        break;
-      case "groups":
-        current = sequences.currentGroupId();
-        break;
-      default:
-        throw die("Unknown sequence name: " + name);
-    }
+    int current =
+        switch (name) {
+          case "changes" -> sequences.currentChangeId();
+          case "accounts" -> sequences.currentAccountId();
+          case "groups" -> sequences.currentGroupId();
+          default -> throw die("Unknown sequence name: " + name);
+        };
     stdout.print(current);
     stdout.print('\n');
     stdout.flush();
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index bf7c154..dd12416 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -244,8 +244,7 @@
     long oldest = now;
 
     for (IoSession s : list) {
-      if (s instanceof MinaSession) {
-        MinaSession minaSession = (MinaSession) s;
+      if (s instanceof MinaSession minaSession) {
         oldest = Math.min(oldest, minaSession.getSession().getCreationTime());
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 5efeb42..a1fbe9c4 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -97,8 +97,7 @@
         acceptor.getManagedSessions().values().stream()
             .sorted(
                 (arg0, arg1) -> {
-                  if (arg0 instanceof MinaSession) {
-                    MinaSession mArg0 = (MinaSession) arg0;
+                  if (arg0 instanceof MinaSession mArg0) {
                     MinaSession mArg1 = (MinaSession) arg1;
                     if (mArg0.getSession().getCreationTime()
                         < mArg1.getSession().getCreationTime()) {
@@ -220,8 +219,8 @@
       return "?";
     }
     String host = null;
-    if (remoteAddress instanceof InetSocketAddress) {
-      final InetSocketAddress sa = (InetSocketAddress) remoteAddress;
+    if (remoteAddress instanceof InetSocketAddress inetSocketAddress) {
+      final InetSocketAddress sa = inetSocketAddress;
       final InetAddress in = sa.getAddress();
       if (numeric) {
         return in.getHostAddress();
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 7f8e719..f43eee2 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -143,23 +143,11 @@
 
   private void print(List<TaskInfo> tasks, long now, boolean viewAll, int threadPoolSize) {
     for (TaskInfo task : tasks) {
-      String start;
-      switch (task.state) {
-        case DONE:
-        case CANCELLED:
-        case PARKED:
-        case STARTING:
-        case RUNNING:
-        case STOPPING:
-        case READY:
-          start = format(task.state);
-          break;
-        case OTHER:
-        case SLEEPING:
-        default:
-          start = time(now, task.delay);
-          break;
-      }
+      String start =
+          switch (task.state) {
+            case DONE, CANCELLED, PARKED, STARTING, RUNNING, STOPPING, READY -> format(task.state);
+            case SLEEPING, OTHER -> time(now, task.delay);
+          };
 
       // Shows information about tasks depending on the user rights
       if (viewAll || task.projectName == null) {
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 344afcb..1759f7b 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -92,6 +92,7 @@
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.experiments.ConfigExperimentFeatures.ConfigExperimentFeaturesModule;
+import com.google.gerrit.server.git.AllUsersRepositoryProvider;
 import com.google.gerrit.server.git.ChangesByProjectCache;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -215,6 +216,8 @@
     bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
     bind(MetricMaker.class).to(DisabledMetricMaker.class);
 
+    // Database resources must outlive the work queue during shutdown.
+    install(new InMemorySchemaModule());
     install(cfgInjector.getInstance(AccountCacheImpl.AccountCacheModule.class));
     install(cfgInjector.getInstance(AccountCacheImpl.AccountCacheBindingModule.class));
 
@@ -273,7 +276,6 @@
     bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON);
     bind(SecureStore.class).to(DefaultSecureStore.class);
 
-    install(new InMemorySchemaModule());
     install(NoSshKeyCache.module());
     install(new GerritInstanceNameModule());
     install(new GerritInstanceIdModule());
@@ -351,6 +353,7 @@
       bind(AllProjectsName.class).toProvider(AllProjectsNameProvider.class).in(SINGLETON);
 
       bind(AllUsersName.class).toProvider(AllUsersNameProvider.class).in(SINGLETON);
+      install(new AllUsersRepositoryProvider.Module());
 
       bind(String.class)
           .annotatedWith(AnonymousCowardName.class)
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 2ba06ba..60a7fcc 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -226,8 +226,7 @@
     char next = '?';
     List<NamedOptionDef> booleans = new ArrayList<>();
     for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.optionsList) {
-      if (handler.option instanceof NamedOptionDef) {
-        NamedOptionDef n = (NamedOptionDef) handler.option;
+      if (handler.option instanceof NamedOptionDef n) {
 
         if (handler instanceof BooleanOptionHandler) {
           booleans.add(n);
@@ -375,8 +374,7 @@
   private static Map<String, OptionHandler> index(List<OptionHandler> in) {
     Map<String, OptionHandler> m = new HashMap<>();
     for (OptionHandler handler : in) {
-      if (handler.option instanceof NamedOptionDef) {
-        NamedOptionDef def = (NamedOptionDef) handler.option;
+      if (handler.option instanceof NamedOptionDef def) {
         if (!def.isArgument()) {
           m.put(def.name(), handler);
           for (String alias : def.aliases()) {
@@ -573,8 +571,7 @@
     @Nullable
     public OptionHandler findOptionByName(String name) {
       for (OptionHandler h : optionsList) {
-        if (h.option instanceof NamedOptionDef) {
-          NamedOptionDef option = (NamedOptionDef) h.option;
+        if (h.option instanceof NamedOptionDef option) {
           if (name.equals(option.name())) {
             return h;
           }
diff --git a/java/com/google/gerrit/util/crypto/AesGcmCipher.java b/java/com/google/gerrit/util/crypto/AesGcmCipher.java
index 1db2d43..f5559cf 100644
--- a/java/com/google/gerrit/util/crypto/AesGcmCipher.java
+++ b/java/com/google/gerrit/util/crypto/AesGcmCipher.java
@@ -16,25 +16,23 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Base64;
 import javax.crypto.Cipher;
+import javax.crypto.KDF;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import org.bouncycastle.crypto.digests.SHA256Digest;
-import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
-import org.bouncycastle.crypto.params.HKDFParameters;
+import javax.crypto.spec.HKDFParameterSpec;
 
 /**
  * Authenticated encryption of short strings with AES-256-GCM.
  *
  * <p>The supplied key material is expanded into a dedicated 256-bit AES key with HKDF-SHA256 (RFC
- * 5869) using BouncyCastle, domain-separated by a caller-provided {@code info} label; the input key
- * bytes are zeroed after derivation. Each value is sealed as {@code "gcm:v1:" + Base64(iv ||
- * ciphertext || tag)} with a fresh 96-bit IV, and the caller supplies opaque additional
+ * 5869) using JDK 25 {@link KDF}, domain-separated by a caller-provided {@code info} label; the
+ * input key bytes are zeroed after derivation. Each value is sealed as {@code "gcm:v1:" + Base64(iv
+ * || ciphertext || tag)} with a fresh 96-bit IV, and the caller supplies opaque additional
  * authenticated data (AAD) that is bound into the tag.
  */
 public final class AesGcmCipher {
@@ -44,8 +42,6 @@
   private static final int IV_BYTES = 12;
   private static final int TAG_BITS = 128;
   private static final int DERIVED_KEY_BYTES = 32;
-  private static final ThreadLocal<Cipher> CIPHERS =
-      ThreadLocal.withInitial(AesGcmCipher::newCipher);
 
   private final SecretKey derivedKey;
 
@@ -60,12 +56,15 @@
       throw new IllegalArgumentException("Master key material must be at least 128 bits.");
     }
     try {
-      byte[] keyBytes = hkdfSha256(masterKeyBytes, info, DERIVED_KEY_BYTES);
-      try {
-        this.derivedKey = new SecretKeySpec(keyBytes, "AES");
-      } finally {
-        Arrays.fill(keyBytes, (byte) 0);
-      }
+      this.derivedKey =
+          KDF.getInstance("HKDF-SHA256")
+              .deriveKey(
+                  "AES",
+                  HKDFParameterSpec.ofExtract()
+                      .addIKM(masterKeyBytes)
+                      .thenExpand(info, DERIVED_KEY_BYTES));
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException("Failed to derive encryption key via HKDF", e);
     } finally {
       Arrays.fill(masterKeyBytes, (byte) 0);
     }
@@ -83,10 +82,9 @@
    */
   public String seal(byte[] aad, String plaintext) {
     try {
-      byte[] iv = new byte[IV_BYTES];
-      Holder.RANDOM.nextBytes(iv);
+      byte[] iv = SecureRandomUtil.newBytes(IV_BYTES);
 
-      Cipher cipher = CIPHERS.get();
+      Cipher cipher = Cipher.getInstance(TRANSFORMATION);
       cipher.init(Cipher.ENCRYPT_MODE, derivedKey, new GCMParameterSpec(TAG_BITS, iv));
       cipher.updateAAD(aad);
 
@@ -98,10 +96,8 @@
         Arrays.fill(pt, (byte) 0);
       }
 
-      byte[] out = new byte[IV_BYTES + ct.length];
-      System.arraycopy(iv, 0, out, 0, IV_BYTES);
-      System.arraycopy(ct, 0, out, IV_BYTES, ct.length);
-      return PREFIX + Base64.getEncoder().encodeToString(out);
+      byte[] payload = ByteBuffer.allocate(IV_BYTES + ct.length).put(iv).put(ct).array();
+      return PREFIX + Base64.getEncoder().encodeToString(payload);
     } catch (GeneralSecurityException e) {
       throw new IllegalStateException("AES-GCM encryption failed", e);
     }
@@ -122,7 +118,8 @@
       if (all.length < IV_BYTES + (TAG_BITS / 8)) {
         throw new IllegalArgumentException("Truncated or corrupted ciphertext payload.");
       }
-      Cipher cipher = CIPHERS.get();
+
+      Cipher cipher = Cipher.getInstance(TRANSFORMATION);
       cipher.init(
           Cipher.DECRYPT_MODE, derivedKey, new GCMParameterSpec(TAG_BITS, all, 0, IV_BYTES));
       cipher.updateAAD(aad);
@@ -139,25 +136,4 @@
       throw new IllegalStateException("AES-GCM decryption failed (wrong key or tampered)", e);
     }
   }
-
-  /** HKDF-SHA256 using BouncyCastle HKDFBytesGenerator. */
-  private static byte[] hkdfSha256(byte[] ikm, byte[] info, int length) {
-    HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());
-    hkdf.init(new HKDFParameters(ikm, null, info));
-    byte[] okm = new byte[length];
-    hkdf.generateBytes(okm, 0, length);
-    return okm;
-  }
-
-  private static Cipher newCipher() {
-    try {
-      return Cipher.getInstance(TRANSFORMATION);
-    } catch (GeneralSecurityException e) {
-      throw new IllegalStateException("Failed to initialize AES-GCM cipher", e);
-    }
-  }
-
-  private static class Holder {
-    private static final SecureRandom RANDOM = new SecureRandom();
-  }
 }
diff --git a/java/com/google/gerrit/util/crypto/BUILD b/java/com/google/gerrit/util/crypto/BUILD
index 9f28cb6..abf732e 100644
--- a/java/com/google/gerrit/util/crypto/BUILD
+++ b/java/com/google/gerrit/util/crypto/BUILD
@@ -6,6 +6,5 @@
     visibility = ["//visibility:public"],
     deps = [
         "//lib:guava",
-        "//lib/bouncycastle:bcprov-neverlink",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
index 1f14880..806baeb 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
@@ -147,7 +147,6 @@
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.RegexAllowedGroupsProvider;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.testing.TestGroupBackend;
 import com.google.gerrit.server.index.account.AccountIndexer;
@@ -1048,8 +1047,8 @@
             // Missing user part
             "@example.com",
 
-            // Non-supported TLD  (see tlds-alpha-by-domain.txt)
-            "new.email@example.africa");
+            // Non-supported TLD
+            "new.email@example.invalid");
     AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
     try (Registration registration =
         extensionRegistry.newRegistration().add(accountIndexedCounter)) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AiReviewPermissionIT.java b/javatests/com/google/gerrit/acceptance/api/change/AiReviewPermissionIT.java
index b8411b6..0e5a783 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AiReviewPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AiReviewPermissionIT.java
@@ -18,17 +18,29 @@
 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.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
 import com.google.inject.Inject;
 import java.util.Map;
 import org.junit.Test;
 
+/**
+ * Tests for the {@code aiReview} change action.
+ *
+ * <p>The action uses an inverted visibility convention: it is emitted only when aiReview is
+ * <em>denied</em> (present, with {@code enabled=false}); when the user is permitted the action is
+ * <em>absent</em>, which the frontend treats as allowed. So throughout these tests {@code
+ * doesNotContainKey(AI_REVIEW)} asserts "allowed" and a present entry with {@code enabled=false}
+ * asserts "denied".
+ */
 public class AiReviewPermissionIT extends AbstractDaemonTest {
 
   private static final String AI_REVIEW = "aiReview";
@@ -40,15 +52,36 @@
   public void aiReviewActionAbsentByDefault() throws Exception {
     String changeId = createChange().getChangeId();
 
+    // All-Projects seeds an explicit grant for Registered Users on
+    // refs/heads/*, so a registered caller is permitted and the action is
+    // absent (allowed).
     Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
 
     assertThat(actions).doesNotContainKey(AI_REVIEW);
   }
 
   @Test
-  public void aiReviewActionDisabledWhenUserNotInGrantedGroup() throws Exception {
+  public void aiReviewActionDisabledWhenSeededGrantRemoved() throws Exception {
     String changeId = createChange().getChangeId();
 
+    // Without the seeded grant the registered caller is denied, so the action
+    // is present with enabled=false (see the class comment on the inverted
+    // absent=allowed / present+disabled=denied convention).
+    removeSeededAiReviewGrant();
+
+    requestScopeOperations.setApiUser(user.id());
+    Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
+
+    assertThat(actions.get(AI_REVIEW).enabled).isFalse();
+  }
+
+  @Test
+  public void aiReviewActionDisabledWhenNotInGrantedGroup() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Drop the seeded grant, then grant only the admin group: a non-admin
+    // registered user is not covered by any ALLOW rule and is denied.
+    removeSeededAiReviewGrant();
     projectOperations
         .project(project)
         .forUpdate()
@@ -177,13 +210,15 @@
   }
 
   @Test
-  public void aiReviewActionDisabledForAdminWhenAdminGroupDenied() throws Exception {
+  public void aiReviewActionDisabledForAdminWhenAdminGroupBlocked() throws Exception {
     String changeId = createChange().getChangeId();
 
+    // Admins are not exempt: a BLOCK on the admin group overrides the seeded
+    // grant (a bare DENY would only cancel an ALLOW for the same group).
     projectOperations
         .project(project)
         .forUpdate()
-        .add(deny(Permission.AI_REVIEW).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(block(Permission.AI_REVIEW).ref("refs/heads/*").group(adminGroupUuid()))
         .update();
 
     requestScopeOperations.setApiUser(admin.id());
@@ -207,4 +242,52 @@
 
     assertThat(actions.get(AI_REVIEW).enabled).isFalse();
   }
+
+  @Test
+  @GerritConfig(
+      name = "experiments.enabled",
+      value = ExperimentFeaturesConstants.ALLOW_AI_REVIEW_FOR_REGISTERED_USERS)
+  public void aiReviewBlockIgnoredWhenExperimentEnabled() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // A BLOCK would normally deny even against an ALLOW, but the experiment is an
+    // unconditional override for identified users: the block is ignored and the
+    // action stays absent (allowed).
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.AI_REVIEW).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
+
+    assertThat(actions).doesNotContainKey(AI_REVIEW);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "experiments.enabled",
+      value = ExperimentFeaturesConstants.ALLOW_AI_REVIEW_FOR_REGISTERED_USERS)
+  public void aiReviewActionAbsentForRegisteredUserWhenExperimentEnabledAndNoGrant()
+      throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Remove the seeded grant: default-deny would deny, but the experiment lets
+    // identified users pass without any rule, so the action is absent (allowed).
+    removeSeededAiReviewGrant();
+
+    requestScopeOperations.setApiUser(user.id());
+    Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
+
+    assertThat(actions).doesNotContainKey(AI_REVIEW);
+  }
+
+  private void removeSeededAiReviewGrant() throws Exception {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .remove(permissionKey(Permission.AI_REVIEW).ref("refs/heads/*"))
+        .update();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 35e2199..132b8bf 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1572,6 +1572,8 @@
 
     // Ensure lastUpdatedOn is updated.
     assertThat(changeOperations.change(change.id()).get().lastUpdatedOn()).isNotEqualTo(oldTs);
+
+    eventRecorder.assertReviewerAddedEvents(change.changeId(), user.email());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index baec17e..44207c9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -22,6 +22,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.EventRecorder;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -279,4 +280,52 @@
     }
     assertThat(gApi.changes().id(changeId.get()).get().isPrivate).isTrue();
   }
+
+  @Test
+  public void reviewerAddedEventFilteredByVisibility() throws Exception {
+    TestAccount reviewer = accountCreator.user2();
+    TestAccount unauthorizedUser = accountCreator.create("unauthorizedUser");
+
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+    String changeId = result.getChangeId();
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setPrivate(true, null);
+
+    try (EventRecorder reviewerRecorder = eventRecorderFactory.create(reviewer);
+        EventRecorder unauthorizedRecorder = eventRecorderFactory.create(unauthorizedUser)) {
+      gApi.changes().id(changeId).addReviewer(reviewer.id().toString());
+
+      reviewerRecorder.assertReviewerAddedEvents(changeId, reviewer.email());
+      unauthorizedRecorder.assertNoReviewerAddedEvents();
+    }
+  }
+
+  @Test
+  public void reviewerAddedEventVisibleWithViewPrivateChangesPermission() throws Exception {
+    TestAccount reviewer = accountCreator.user2();
+    TestAccount otherUser = accountCreator.create("otherUser");
+
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+    String changeId = result.getChangeId();
+
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId).setPrivate(true, null);
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    try (EventRecorder otherUserRecorder = eventRecorderFactory.create(otherUser)) {
+      gApi.changes().id(changeId).addReviewer(reviewer.id().toString());
+
+      otherUserRecorder.assertReviewerAddedEvents(changeId, reviewer.email());
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java b/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java
index 0609b5c..d172838 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java
@@ -57,13 +57,12 @@
   @Test
   @GerritConfig(
       name = "experiments.enabled",
-      values = {"GerritBackendFeature__attach_nonce_to_documentation"})
+      values = {"GerritBackendFeature__allow_fix_suggestions_in_comments"})
   public void getEnabled() throws Exception {
     ExperimentInfo experimentInfo =
         gApi.config()
             .server()
-            .experiment(
-                ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+            .experiment(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS)
             .get();
     assertThat(experimentInfo.enabled).isTrue();
   }
@@ -73,8 +72,7 @@
     ExperimentInfo experimentInfo =
         gApi.config()
             .server()
-            .experiment(
-                ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+            .experiment(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS)
             .get();
     assertThat(experimentInfo.enabled).isFalse();
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
index a32e0c0..2e067ab 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
@@ -48,7 +48,6 @@
     assertThat(experiments.keySet())
         .containsAtLeast(
             ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS,
-            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
             ExperimentFeaturesConstants.SKIP_SUBMIT_RECORDS_WITHOUT_SUBMIT_REQUIREMENTS)
         .inOrder();
 
@@ -57,8 +56,7 @@
         .isFalse();
     assertThat(
             experiments.get(
-                    ExperimentFeaturesConstants
-                        .GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+                    ExperimentFeaturesConstants.SKIP_SUBMIT_RECORDS_WITHOUT_SUBMIT_REQUIREMENTS)
                 .enabled)
         .isFalse();
   }
@@ -66,13 +64,12 @@
   @Test
   @GerritConfig(
       name = "experiments.enabled",
-      values = {"GerritBackendFeature__attach_nonce_to_documentation"})
+      values = {"GerritBackendFeature__allow_fix_suggestions_in_comments"})
   public void listEnabled_noneEnabled() throws Exception {
     ImmutableMap<String, ExperimentInfo> experiments =
         gApi.config().server().listExperiments().enabledOnly().get();
     assertThat(experiments.keySet())
-        .containsExactly(
-            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION);
+        .containsExactly(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS);
     for (ExperimentInfo experimentInfo : experiments.values()) {
       assertThat(experimentInfo.enabled).isTrue();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
index eeeef99..5f29062 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
@@ -71,7 +71,6 @@
 import com.google.gerrit.server.group.testing.TestGroupBackend;
 import com.google.gerrit.server.permissions.RegexPermissionPolicy;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.schema.GrantPostReviewCommentPermission;
 import com.google.gerrit.server.schema.GrantRevertPermission;
 import com.google.inject.Inject;
 import java.util.Arrays;
@@ -103,7 +102,6 @@
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ExtensionRegistry extensionRegistry;
   @Inject private GrantRevertPermission grantRevertPermission;
-  @Inject private GrantPostReviewCommentPermission grantPostReviewCommentPermission;
 
   private Project.NameKey newProjectName;
 
@@ -215,67 +213,6 @@
   }
 
   @Test
-  public void grantPostReviewCommentPermission() throws Exception {
-    String ref = "refs/*";
-    String groupId = "global:Registered-Users";
-
-    grantPostReviewCommentPermission.execute(newProjectName);
-
-    ProjectAccessInfo info = pApi().access();
-    assertThat(info.local.containsKey(ref)).isTrue();
-    AccessSectionInfo accessSectionInfo = info.local.get(ref);
-    assertThat(accessSectionInfo.permissions.containsKey(Permission.POST_REVIEW_COMMENT)).isTrue();
-    PermissionInfo permissionInfo =
-        accessSectionInfo.permissions.get(Permission.POST_REVIEW_COMMENT);
-    assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
-    PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
-    assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
-  }
-
-  @Test
-  public void grantPostReviewCommentPermissionOnlyWorksOnce() throws Exception {
-    grantPostReviewCommentPermission.execute(newProjectName);
-    grantPostReviewCommentPermission.execute(newProjectName);
-
-    try (Repository repo = repoManager.openRepository(newProjectName)) {
-      MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
-      ProjectConfig projectConfig = projectConfigFactory.read(md);
-      AccessSection all = projectConfig.getAccessSection(AccessSection.ALL);
-
-      Permission permission = all.getPermission(Permission.POST_REVIEW_COMMENT);
-      assertThat(permission.getRules()).hasSize(1);
-    }
-  }
-
-  @Test
-  public void grantPostReviewCommentPermissionDoesntOverrideAdminsPreferences() throws Exception {
-    GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
-
-    try (Repository repo = repoManager.openRepository(newProjectName)) {
-      MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
-      ProjectConfig projectConfig = projectConfigFactory.read(md);
-      projectConfig.upsertAccessSection(
-          AccessSection.ALL,
-          all -> {
-            grant(projectConfig, all, Permission.POST_REVIEW_COMMENT, otherGroup);
-          });
-      md.getCommitBuilder().setAuthor(admin.newIdent());
-      md.getCommitBuilder().setCommitter(admin.newIdent());
-      md.setMessage("Add Post Review Comment permission for anonymous users\n");
-
-      projectConfig.commit(md);
-    }
-    projectCache.evict(newProjectName);
-    ProjectAccessInfo expected = pApi().access();
-
-    grantPostReviewCommentPermission.execute(newProjectName);
-    projectCache.evict(newProjectName);
-    ProjectAccessInfo actual = pApi().access();
-    // Permissions don't change.
-    assertThat(actual.local).isEqualTo(expected.local);
-  }
-
-  @Test
   public void getDefaultInheritance() throws Exception {
     String inheritedName = pApi().access().inheritsFrom.name;
     assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 58f6605..6180157 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -75,6 +75,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -502,6 +503,19 @@
   }
 
   @Test
+  public void hiddenProjectIsNotFoundForNonOwner() throws Exception {
+    ConfigInput ci = new ConfigInput();
+    ci.state = ProjectState.HIDDEN;
+    gApi.projects().name(project.get()).config(ci);
+
+    requestScopeOperations.setApiUser(user.id());
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.projects().name(project.get()).get());
+    assertThat(thrown).hasMessageThat().doesNotContain("HIDDEN");
+  }
+
+  @Test
   public void nonActiveProjectCanBeMadeActiveByHostAdmin() throws Exception {
     // ACTIVE => HIDDEN
     ConfigInput ci1 = new ConfigInput();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
index 62140ed..1348023 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
@@ -17,6 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.common.BlameInfo;
@@ -152,4 +154,213 @@
     // File doesn't exist in base commit.
     assertThat(blameInfos).isEmpty();
   }
+
+  @Test
+  public void withIgnoreRevsFile() throws Exception {
+    PushOneCommit.Result r1 = createChange("Change 1", "foo.txt", "line1\nline2\n");
+    PushOneCommit.Result r2 = createChange("Change 2", "foo.txt", "line1\nline2 formatted\n");
+    PushOneCommit.Result r3 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "Change 3",
+                ImmutableMap.of(
+                    "foo.txt",
+                    "line1\nline2 formatted\nline3\n",
+                    ".git-blame-ignore-revs",
+                    "# Formatting commit\n" + r2.getCommit().name() + "\n"))
+            .to("refs/for/master");
+
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r3.getChangeId()).current().file("foo.txt").blameRequest().get();
+
+    assertThat(blameInfos).hasSize(2);
+    BlameInfo first = getBlameInfoForCommit(blameInfos, r1.getCommit().name());
+    assertThat(first.ranges).hasSize(1);
+    assertThat(first.ranges.get(0).start).isEqualTo(1);
+    assertThat(first.ranges.get(0).end).isEqualTo(2);
+
+    BlameInfo second = getBlameInfoForCommit(blameInfos, r3.getCommit().name());
+    assertThat(second.ranges).hasSize(1);
+    assertThat(second.ranges.get(0).start).isEqualTo(3);
+    assertThat(second.ranges.get(0).end).isEqualTo(3);
+  }
+
+  @Test
+  public void withIgnoreRevsFileFromBase() throws Exception {
+    PushOneCommit.Result r1 = createChange("Change 1", "foo.txt", "line1\nline2\n");
+    PushOneCommit.Result r2 = createChange("Change 2", "foo.txt", "line1\nline2 formatted\n");
+    PushOneCommit.Result r3 =
+        createChange("Change 3", ".git-blame-ignore-revs", r2.getCommit().name() + "\n");
+    PushOneCommit.Result r4 =
+        createChange("Change 4", "foo.txt", "line1\nline2 formatted\nline3\n");
+
+    // Base of r4 is r3, which has .git-blame-ignore-revs ignoring r2.
+    List<BlameInfo> blameAtR3 =
+        gApi.changes()
+            .id(r4.getChangeId())
+            .current()
+            .file("foo.txt")
+            .blameRequest()
+            .forBase(true)
+            .get();
+    assertThat(blameAtR3).hasSize(1);
+    assertThat(blameAtR3.get(0).id).isEqualTo(r1.getCommit().name());
+    assertThat(blameAtR3.get(0).ranges).hasSize(1);
+    assertThat(blameAtR3.get(0).ranges.get(0).start).isEqualTo(1);
+    assertThat(blameAtR3.get(0).ranges.get(0).end).isEqualTo(2);
+
+    // Base of r3 is r2, which does not have .git-blame-ignore-revs yet.
+    List<BlameInfo> blameAtR2 =
+        gApi.changes()
+            .id(r3.getChangeId())
+            .current()
+            .file("foo.txt")
+            .blameRequest()
+            .forBase(true)
+            .get();
+    assertThat(blameAtR2).hasSize(2);
+    BlameInfo r1Blame = getBlameInfoForCommit(blameAtR2, r1.getCommit().name());
+    assertThat(r1Blame.ranges).hasSize(1);
+    assertThat(r1Blame.ranges.get(0).start).isEqualTo(1);
+    assertThat(r1Blame.ranges.get(0).end).isEqualTo(1);
+    BlameInfo r2Blame = getBlameInfoForCommit(blameAtR2, r2.getCommit().name());
+    assertThat(r2Blame.ranges).hasSize(1);
+    assertThat(r2Blame.ranges.get(0).start).isEqualTo(2);
+    assertThat(r2Blame.ranges.get(0).end).isEqualTo(2);
+  }
+
+  @Test
+  public void withIgnoreRevsFileAddedInSubsequentPatchSet() throws Exception {
+    PushOneCommit.Result r1 = createChange("Change 1", "foo.txt", "line1\nline2\n");
+    PushOneCommit.Result r2 = createChange("Change 2", "foo.txt", "line1\nline2 formatted\n");
+    PushOneCommit.Result ps1 =
+        createChange("Change 3", "foo.txt", "line1\nline2 formatted\nline3\n");
+
+    // Patch set 1 does not have .git-blame-ignore-revs, so line 2 is blamed on r2.
+    List<BlameInfo> ps1Blame =
+        gApi.changes().id(ps1.getChangeId()).revision(1).file("foo.txt").blameRequest().get();
+    assertThat(ps1Blame).hasSize(3);
+    assertThat(getBlameInfoForCommit(ps1Blame, r2.getCommit().name()).ranges.get(0).start)
+        .isEqualTo(2);
+
+    // Patch set 2 introduces .git-blame-ignore-revs ignoring r2.
+    PushOneCommit.Result ps2 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "Change 3",
+                ImmutableMap.of(
+                    "foo.txt",
+                    "line1\nline2 formatted\nline3\n",
+                    ".git-blame-ignore-revs",
+                    r2.getCommit().name() + "\n"),
+                ps1.getChangeId())
+            .to("refs/for/master");
+    ps2.assertOkStatus();
+
+    List<BlameInfo> ps2Blame =
+        gApi.changes().id(ps2.getChangeId()).current().file("foo.txt").blameRequest().get();
+    assertThat(ps2Blame).hasSize(2);
+    BlameInfo first = getBlameInfoForCommit(ps2Blame, r1.getCommit().name());
+    assertThat(first.ranges).hasSize(1);
+    assertThat(first.ranges.get(0).start).isEqualTo(1);
+    assertThat(first.ranges.get(0).end).isEqualTo(2);
+
+    BlameInfo second = getBlameInfoForCommit(ps2Blame, ps2.getCommit().name());
+    assertThat(second.ranges).hasSize(1);
+    assertThat(second.ranges.get(0).start).isEqualTo(3);
+    assertThat(second.ranges.get(0).end).isEqualTo(3);
+  }
+
+  @Test
+  public void withIgnoreRevsFileInMergeCommit() throws Exception {
+    PushOneCommit.Result r1 = createChange("Change 1", "foo.txt", "line1\nline2\n");
+    PushOneCommit.Result r2 = createChange("Change 2", "foo.txt", "line1\nline2 formatted\n");
+
+    PushOneCommit.Result p1 =
+        createChange("Parent 1", ".git-blame-ignore-revs", r2.getCommit().name() + "\n");
+
+    testRepo.reset(r2.getCommit());
+    PushOneCommit.Result p2 = createChange("Parent 2", "other.txt", "other\n");
+
+    PushOneCommit merge =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Merge Change",
+            ImmutableMap.of(
+                "foo.txt",
+                "line1\nline2 formatted\nline3\n",
+                ".git-blame-ignore-revs",
+                r2.getCommit().name() + "\n",
+                "other.txt",
+                "other\n"));
+    merge.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
+    PushOneCommit.Result mergeResult = merge.to("refs/for/master");
+    mergeResult.assertOkStatus();
+
+    // Blame on the merge commit itself.
+    List<BlameInfo> mergeBlame =
+        gApi.changes().id(mergeResult.getChangeId()).current().file("foo.txt").blameRequest().get();
+    assertThat(mergeBlame).hasSize(2);
+    BlameInfo r1Blame = getBlameInfoForCommit(mergeBlame, r1.getCommit().name());
+    assertThat(r1Blame.ranges).hasSize(1);
+    assertThat(r1Blame.ranges.get(0).start).isEqualTo(1);
+    assertThat(r1Blame.ranges.get(0).end).isEqualTo(2);
+
+    // Blame on the automerge base of the 2-parent merge commit.
+    List<BlameInfo> baseBlame =
+        gApi.changes()
+            .id(mergeResult.getChangeId())
+            .current()
+            .file("foo.txt")
+            .blameRequest()
+            .forBase(true)
+            .get();
+    assertThat(baseBlame).hasSize(1);
+    assertThat(baseBlame.get(0).id).isEqualTo(r1.getCommit().name());
+    assertThat(baseBlame.get(0).ranges).hasSize(1);
+    assertThat(baseBlame.get(0).ranges.get(0).start).isEqualTo(1);
+    assertThat(baseBlame.get(0).ranges.get(0).end).isEqualTo(2);
+  }
+
+  @Test
+  public void withIgnoreRevsFileInlineCommentsAndInvalidLines() throws Exception {
+    PushOneCommit.Result r1 = createChange("Change 1", "foo.txt", "line1\nline2\n");
+    PushOneCommit.Result r2 = createChange("Change 2", "foo.txt", "line1\nline2 formatted\n");
+
+    // Create a sibling commit that is not an ancestor of r3.
+    PushOneCommit.Result nonAncestor = createChange("Unrelated", "other.txt", "other\n");
+    testRepo.reset(r2.getCommit());
+
+    PushOneCommit.Result r3 =
+        createChange(
+            "Change 3",
+            ".git-blame-ignore-revs",
+            "# Header comment\n\nnot-a-valid-sha\n"
+                + nonAncestor.getCommit().name()
+                + "\n"
+                + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n"
+                + r2.getCommit().name()
+                + "\t# tab-separated inline comment\n");
+
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r3.getChangeId()).current().file("foo.txt").blameRequest().get();
+
+    assertThat(blameInfos).hasSize(1);
+    assertThat(blameInfos.get(0).id).isEqualTo(r1.getCommit().name());
+    assertThat(blameInfos.get(0).ranges).hasSize(1);
+    assertThat(blameInfos.get(0).ranges.get(0).start).isEqualTo(1);
+    assertThat(blameInfos.get(0).ranges.get(0).end).isEqualTo(2);
+  }
+
+  private static BlameInfo getBlameInfoForCommit(List<BlameInfo> blameInfos, String commitId) {
+    return blameInfos.stream()
+        .filter(b -> commitId.equals(b.id))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("No BlameInfo found for commit " + commitId));
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 4a740cd..4568001 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -1344,6 +1344,17 @@
     String changeId = GitUtil.getChangeId(testRepo, c).get();
     assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email());
     assertThat(getReviewerEmails(changeId, ReviewerState.CC)).isEmpty();
+
+    // Create a replacement commit (patch set 2) with forged author and committer.
+    commitBuilder()
+        .author(user.newIdent())
+        .committer(user2.newIdent())
+        .add(PushOneCommit.FILE_NAME, "new content")
+        .message(PushOneCommit.SUBJECT + "\n\nChange-Id: " + changeId)
+        .create();
+    pushHead(testRepo, "refs/for/master");
+
+    assertThat(getReviewerEmails(changeId, ReviewerState.CC)).isEmpty();
   }
 
   @Test
@@ -3749,7 +3760,7 @@
         .add(allow(Permission.FORGE_AUTHOR).ref(ref).group(groupUuid))
         .add(allow(Permission.FORGE_COMMITTER).ref(ref).group(groupUuid))
         .add(allow(Permission.FORGE_SERVER).ref(ref).group(groupUuid))
-        .add(allow(Permission.PUSH_MERGE).ref("refs/for/" + ref).group(groupUuid))
+        .add(allow(Permission.PUSH_MERGE).ref(ref).group(groupUuid))
         .update();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 0d6115b..6ba9d3f 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 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.git.testing.PushResultSubject.assertThat;
@@ -25,6 +26,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -33,6 +35,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -44,6 +47,7 @@
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
@@ -126,6 +130,187 @@
   }
 
   @Test
+  public void pushMergeCommitDirectlyWithPushMergePermissionOnTargetRef() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit().to("refs/heads/master").assertOkStatus();
+  }
+
+  @Test
+  public void pushMergeCommitDirectlyWithLegacyPushMergePermission() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit().to("refs/heads/master").assertOkStatus();
+  }
+
+  @Test
+  public void pushMergeCommitDirectlyWithoutPushMergePermissionDenied() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit()
+        .to("refs/heads/master")
+        .assertErrorStatus("you are not allowed to upload merges");
+  }
+
+  @Test
+  public void pushMergeCommitDirectlyWithoutPushPermissionDenied() throws Exception {
+    // Push Merge Commit is an add-on to Push (see access-control.txt): granting it without Push is
+    // not sufficient, so the push is rejected at the ref-update check before the merge validator.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit()
+        .to("refs/heads/master")
+        .assertErrorStatus("prohibited by Gerrit: not permitted: update");
+  }
+
+  @Test
+  public void pushMergeCommitForReviewDoesNotUseTargetRefPermission() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit()
+        .to("refs/for/master")
+        .assertErrorStatus("you are not allowed to upload merges");
+  }
+
+  @Test
+  public void pushMergeCommitForReviewWithPushMergePermissionOnReviewRef() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createMergeCommit().to("refs/for/master").assertOkStatus();
+  }
+
+  @Test
+  public void pushForgedAuthorWithoutForgeAuthorPermissionDenied() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(block(Permission.FORGE_AUTHOR).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    RevCommit initialHead =
+        testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+    pushFactory
+        .create(
+            new PersonIdent("Forged Author", "forged@example.com"),
+            testRepo,
+            "forged author\n\nChange-Id: I0000000000000000000000000000000000000004",
+            "a.txt",
+            "a")
+        .setParents(ImmutableList.of(initialHead))
+        .to("refs/heads/master")
+        .assertErrorStatus("invalid author");
+  }
+
+  @Test
+  public void pushForgedCommitterWithoutForgeCommitterPermissionDenied() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(block(Permission.FORGE_COMMITTER).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    RevCommit initialHead =
+        testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+    pushFactory
+        .create(
+            new PersonIdent("Forged Committer", "forged@example.com"),
+            testRepo,
+            "forged committer\n\nChange-Id: I0000000000000000000000000000000000000005",
+            "b.txt",
+            "b")
+        .setParents(ImmutableList.of(initialHead))
+        .to("refs/heads/master")
+        .assertErrorStatus("invalid committer");
+  }
+
+  @Test
+  public void amendGerritAuthoredMergeWithoutForgeServerPermissionDenied() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createGerritAuthoredMerge("I0000000000000000000000000000000000000006")
+        .to("refs/heads/master")
+        .assertErrorStatus("requires 'FORGE_SERVER' permission");
+  }
+
+  @Test
+  public void amendGerritAuthoredMergeWithForgeServerPermissionAllowed() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_SERVER).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    createGerritAuthoredMerge("I0000000000000000000000000000000000000007")
+        .to("refs/heads/master")
+        .assertOkStatus();
+  }
+
+  @Test
+  public void mergedOptionForMergeCommitUsesReviewMergePermission() throws Exception {
+    // Land a merge directly on master, then push it for review with %merged. The merged-commits
+    // validation checks Push Merge Commit on the refs/for review ref, not the destination ref, so
+    // without a refs/for grant the review upload is rejected even though the branch grant exists.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    PushOneCommit.Result merged = createMergeCommit().to("refs/heads/master");
+    merged.assertOkStatus();
+
+    testRepo.reset(merged.getCommit());
+    String ref = "refs/for/master%merged";
+    PushResult pr = pushHead(testRepo, ref, false);
+    RemoteRefUpdate rru = pr.getRemoteUpdate(ref);
+    assertWithMessage(rru.getMessage())
+        .that(rru.getStatus())
+        .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+    assertThat(rru.getMessage()).contains("you are not allowed to upload merges");
+  }
+
+  @Test
   public void fastForwardUpdateDenied() throws Exception {
     testRepo.branch("HEAD").commit().create();
     PushResult r = push("HEAD:refs/heads/master");
@@ -365,6 +550,43 @@
   }
 
   @Test
+  public void skipValidationWithPushMergePermissionOnTargetRef() throws Exception {
+    // Skip-validation's Push Merge Commit component is scoped to the destination ref.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_SERVER).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    testRepo.branch("HEAD").commit().create();
+    PushResult r =
+        push(c -> c.setPushOptions(ImmutableList.of("skip-validation")), "HEAD:refs/heads/master");
+    assertThat(r).onlyRef("refs/heads/master").isOk();
+  }
+
+  @Test
+  public void skipValidationWithPushMergePermissionOnReviewRefDenied() throws Exception {
+    // A refs/for Push Merge Commit grant no longer satisfies skip-validation; it is scoped to the
+    // destination ref.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_SERVER).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    testRepo.branch("HEAD").commit().create();
+    PushResult r =
+        push(c -> c.setPushOptions(ImmutableList.of("skip-validation")), "HEAD:refs/heads/master");
+    assertThat(r)
+        .onlyRef("refs/heads/master")
+        .isRejected("prohibited by Gerrit: not permitted: skip validation");
+  }
+
+  @Test
   public void accessDatabaseForNoteDbDenied() throws Exception {
     projectOperations
         .project(project)
@@ -506,4 +728,32 @@
     }
     return u.getNewObjectId();
   }
+
+  private PushOneCommit createGerritAuthoredMerge(String changeId) throws Exception {
+    RevCommit initialHead =
+        testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+    RevCommit secondParent = testRepo.branch("side").commit().create();
+    return pushFactory
+        .create(
+            serverIdent.get(),
+            testRepo,
+            "merge by server\n\nChange-Id: " + changeId,
+            "server-merge.txt",
+            "server-merge")
+        .setParents(ImmutableList.of(initialHead, secondParent));
+  }
+
+  private PushOneCommit createMergeCommit() throws Exception {
+    RevCommit initialHead =
+        testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+    RevCommit secondParent = testRepo.branch("side").commit().create();
+    return pushFactory
+        .create(
+            admin.newIdent(),
+            testRepo,
+            "merge commit\n\nChange-Id: I0000000000000000000000000000000000000001",
+            "merge.txt",
+            "merge")
+        .setParents(ImmutableList.of(initialHead, secondParent));
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index 9a69013..8f011f8 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -45,7 +45,6 @@
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.Collection;
 import java.util.function.Consumer;
 import org.eclipse.jgit.lib.Config;
@@ -65,8 +64,8 @@
   public void reindexFromScratch() throws Exception {
     setUpChange();
 
-    MoreFiles.deleteRecursively(sitePaths.resolve("index"), RecursiveDeleteOption.ALLOW_INSECURE);
-    Files.createDirectory(sitePaths.resolve("index"));
+    MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+    Files.createDirectory(sitePaths.index_dir);
     assertServerStartupFails();
 
     runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
@@ -112,8 +111,8 @@
     updateConfig(config -> config.setBoolean("index", null, "reuseExistingDocuments", true));
     setUpChange();
 
-    MoreFiles.deleteRecursively(sitePaths.resolve("index"), RecursiveDeleteOption.ALLOW_INSECURE);
-    Files.createDirectory(sitePaths.resolve("index"));
+    MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+    Files.createDirectory(sitePaths.index_dir);
     assertServerStartupFails();
 
     runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
@@ -122,7 +121,7 @@
     runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
     assertIndexQueries();
 
-    Files.copy(sitePaths.resolve("index"), sitePaths.resolve("index-backup"));
+    Files.copy(sitePaths.index_dir, sitePaths.resolve("index-backup"));
     try (ServerContext ctx = startServer()) {
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
       gApi.changes().id(changeId).revision(1).review(ReviewInput.approve());
@@ -130,8 +129,8 @@
       assertThat(gApi.changes().query("label:Code-Review+2").get().stream().map(c -> c.changeId))
           .containsExactly(changeId);
     }
-    MoreFiles.deleteRecursively(sitePaths.resolve("index"), RecursiveDeleteOption.ALLOW_INSECURE);
-    Files.copy(sitePaths.resolve("index-backup"), sitePaths.resolve("index"));
+    MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+    Files.copy(sitePaths.resolve("index-backup"), sitePaths.index_dir);
     runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
     try (ServerContext ctx = startServer()) {
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -140,30 +139,6 @@
     }
   }
 
-  @Test
-  public void reindexWithCustomIndexDirectory() throws Exception {
-    // Create a change against default index location, then update index.directory
-    setUpChange();
-    updateConfig(config -> config.setString("index", null, "directory", "custom-index"));
-
-    // Gerrit will refuse to start until that directory is (re)indexed.
-    assertServerStartupFails();
-
-    runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
-    Path customIndexDir = sitePaths.resolve("custom-index");
-    assertWithMessage("custom index.directory directory")
-        .that(Files.exists(customIndexDir))
-        .isTrue();
-    assertWithMessage("gerrit_index.config under custom index.directory")
-        .that(Files.exists(customIndexDir.resolve("gerrit_index.config")))
-        .isTrue();
-
-    GerritIndexStatus status = new GerritIndexStatus(customIndexDir);
-    assertThat(status.getReady(CHANGES, ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion()))
-        .isTrue();
-    assertIndexQueries();
-  }
-
   private void assertIndexQueries() throws Exception {
     try (ServerContext ctx = startServer()) {
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -278,7 +253,7 @@
     int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
 
     // Before storing any changes, switch back to the previous version.
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths.resolve("index"));
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
     status.setReady(CHANGES, currVersion, false);
     status.setReady(CHANGES, prevVersion, true);
     status.save();
@@ -386,7 +361,7 @@
   private void assertReady(int expectedReady) throws Exception {
     ImmutableSortedSet<Integer> allVersions =
         ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths.resolve("index"));
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
     assertWithMessage("ready state for index versions")
         .that(
             allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 5a648ca..b79977e 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -72,13 +72,13 @@
 
     // Simulate a projects indexes files modified in the past by 3 seconds
     Optional<Instant> projectsLastModified =
-        getProjectsIndexLastModified(sitePaths.resolve("index")).map(t -> t.minusSeconds(3));
+        getProjectsIndexLastModified(sitePaths.index_dir).map(t -> t.minusSeconds(3));
     assertThat(projectsLastModified).isPresent();
-    setProjectsIndexLastModifiedInThePast(sitePaths.resolve("index"), projectsLastModified.get());
+    setProjectsIndexLastModifiedInThePast(sitePaths.index_dir, projectsLastModified.get());
 
     initSite();
     Optional<Instant> projectsLastModifiedAfterInit =
-        getProjectsIndexLastModified(sitePaths.resolve("index"));
+        getProjectsIndexLastModified(sitePaths.index_dir);
 
     // Verify that projects index files haven't been updated
     assertThat(projectsLastModified).isEqualTo(projectsLastModifiedAfterInit);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/Schema_187IT.java b/javatests/com/google/gerrit/acceptance/pgm/Schema_187IT.java
new file mode 100644
index 0000000..107e258
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/Schema_187IT.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.entities.Permission.AI_REVIEW;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.schema.NoteDbSchemaVersion;
+import com.google.gerrit.server.schema.Schema_187;
+import com.google.gerrit.testing.TestUpdateUI;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+@Sandboxed
+public class Schema_187IT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private NoteDbSchemaVersion.Arguments args;
+
+  private final TestUpdateUI testUpdateUI = new TestUpdateUI();
+
+  @Test
+  public void grantsAiReviewOnRefsHeadsWhenMissing() throws Exception {
+    // AbstractDaemonTest bootstraps All-Projects with the seed already present;
+    // remove it so the migration has work to do (mirrors an upgrade from before
+    // the permission existed).
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(permissionKey(AI_REVIEW).ref("refs/heads/*"))
+        .update();
+    assertThat(aiReviewRule()).isEmpty();
+
+    RevCommit oldHead = configHead();
+    runMigration();
+
+    assertThat(configHead()).isNotEqualTo(oldHead);
+    assertThat(aiReviewRule()).containsExactly("group Registered Users");
+  }
+
+  @Test
+  public void skipsWhenAiReviewRuleAlreadyExists() throws Exception {
+    // The bootstrap seed is present by default; the migration must not overwrite
+    // an existing rule (idempotent, admin customizations preserved).
+    assertThat(aiReviewRule()).isNotEmpty();
+
+    RevCommit oldHead = configHead();
+    runMigration();
+
+    assertThat(configHead()).isEqualTo(oldHead);
+  }
+
+  private void runMigration() throws Exception {
+    new Schema_187().upgrade(args, testUpdateUI);
+  }
+
+  private RevCommit configHead() {
+    return projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG);
+  }
+
+  private List<String> aiReviewRule() {
+    Config cfg = projectOperations.project(allProjects).getConfig();
+    return Arrays.asList(cfg.getStringList("access", "refs/heads/*", AI_REVIEW));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index 7d5280f..2655dea 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -240,6 +242,50 @@
   }
 
   @Test
+  public void addWatchWhenExistingWatchedProjectIsHidden_reportsProjectAsNotFound()
+      throws Exception {
+    String hiddenProject = projectOperations.newProject().create().get();
+    String otherProject = projectOperations.newProject().create().get();
+
+    requestScopeOperations.setApiUser(user.id());
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+    ProjectWatchInfo hiddenPwi = new ProjectWatchInfo();
+    hiddenPwi.project = hiddenProject;
+    projectsToWatch.add(hiddenPwi);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    hideProject(hiddenProject);
+
+    requestScopeOperations.setApiUser(user.id());
+    ProjectWatchInfo otherPwi = new ProjectWatchInfo();
+    otherPwi.project = otherProject;
+    projectsToWatch.add(otherPwi);
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.accounts().self().setWatchedProjects(projectsToWatch));
+    assertThat(thrown).hasMessageThat().isEqualTo("Project Not Found: " + hiddenProject);
+  }
+
+  @Test
+  public void getWatchedProjectsReportsHiddenProjectAsNotFound() throws Exception {
+    String hiddenProject = projectOperations.newProject().create().get();
+
+    requestScopeOperations.setApiUser(user.id());
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = hiddenProject;
+    gApi.accounts().self().setWatchedProjects(Lists.newArrayList(pwi));
+
+    hideProject(hiddenProject);
+
+    requestScopeOperations.setApiUser(user.id());
+    List<ProjectWatchInfo> watchedProjects = gApi.accounts().self().getWatchedProjects();
+    assertThat(watchedProjects).hasSize(1);
+    assertThat(watchedProjects.getFirst().project).isEqualTo(hiddenProject);
+    assertThat(watchedProjects.getFirst().problem).isEqualTo("Project Not Found: " + hiddenProject);
+  }
+
+  @Test
   public void deleteNonExistingProjectWatch() throws Exception {
     String projectName = project.get();
 
@@ -360,4 +406,11 @@
             () -> gApi.accounts().self().setWatchedProjects(projectsToWatch));
     assertThat(t.getMessage()).isEqualTo("project name must be specified");
   }
+
+  private void hideProject(String projectName) throws Exception {
+    requestScopeOperations.setApiUser(admin.id());
+    ConfigInput configInput = new ConfigInput();
+    configInput.state = ProjectState.HIDDEN;
+    gApi.projects().name(projectName).config(configInput);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 8141f54..fd3d7bd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -48,14 +48,28 @@
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Locale;
 import java.util.stream.IntStream;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
+
+  /**
+   * Runs all tests with a real FanOut thread pool, so that reviewer visibility checks run
+   * concurrently. The default test configuration uses a direct executor.
+   */
+  @ConfigSuite.Config
+  public static Config fanOutThreadPool() {
+    Config cfg = new Config();
+    cfg.setInt("execution", null, "fanOutThreadPoolSize", 4);
+    return cfg;
+  }
+
   @Inject private AccountOperations accountOperations;
   @Inject private GroupOperations groupOperations;
   @Inject private ProjectOperations projectOperations;
@@ -737,6 +751,91 @@
         .inOrder();
   }
 
+  @Test
+  public void suggestReviewers_mixedVisibility_preservesRankingAndLimit() throws Exception {
+    MixedVisibilitySetup setup = setUpMixedVisibility();
+
+    requestScopeOperations.setApiUser(setup.caller.id());
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), null, 2);
+
+    // Reviewers are ranked by how often they reviewed the caller's changes. Odd-ranked
+    // reviewers cannot see the change, so the first 2 visible reviewers are ranks 0 and 2.
+    assertThat(accountIds(reviewers))
+        .containsExactly(setup.ranked.get(0).id().get(), setup.ranked.get(2).id().get())
+        .inOrder();
+  }
+
+  @Test
+  public void suggestReviewers_manyInvisibleCandidates_allVisibleFound() throws Exception {
+    MixedVisibilitySetup setup = setUpMixedVisibility();
+
+    requestScopeOperations.setApiUser(setup.caller.id());
+    List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), null, 10);
+
+    // The limit exceeds the number of visible candidates, so every candidate is checked and all
+    // visible ones are returned in ranking order.
+    assertThat(accountIds(reviewers))
+        .containsExactly(
+            setup.ranked.get(0).id().get(),
+            setup.ranked.get(2).id().get(),
+            setup.ranked.get(4).id().get())
+        .inOrder();
+  }
+
+  private static class MixedVisibilitySetup {
+    final TestAccount caller;
+    final ImmutableList<TestAccount> ranked;
+
+    MixedVisibilitySetup(TestAccount caller, ImmutableList<TestAccount> ranked) {
+      this.caller = caller;
+      this.ranked = ranked;
+    }
+  }
+
+  /**
+   * Creates 6 reviewers of the caller's recent changes. The reviewer at index i reviewed (6 - i)
+   * changes, so the default suggestion ranks the reviewers in index order. Afterwards, only the
+   * caller and the even-ranked reviewers can read the project.
+   */
+  private MixedVisibilitySetup setUpMixedVisibility() throws Exception {
+    TestAccount caller = user("mixedcaller", "Mixed Caller");
+    ImmutableList.Builder<TestAccount> rankedBuilder = ImmutableList.builder();
+    for (int i = 0; i < 6; i++) {
+      rankedBuilder.add(user("mixedreviewer" + i, "Mixed Reviewer " + i));
+    }
+    ImmutableList<TestAccount> ranked = rankedBuilder.build();
+
+    requestScopeOperations.setApiUser(caller.id());
+    for (int c = 0; c < ranked.size(); c++) {
+      String changeId = createChangeFromApi();
+      for (int r = 0; r < ranked.size() - c; r++) {
+        reviewChange(changeId, ranked.get(r));
+      }
+    }
+
+    requestScopeOperations.setApiUser(admin.id());
+    AccountGroup.UUID readers =
+        groupOperations
+            .newGroup()
+            .name(name("readers"))
+            .members(caller.id(), ranked.get(0).id(), ranked.get(2).id(), ranked.get(4).id())
+            .create();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(readers))
+        .update();
+    return new MixedVisibilitySetup(caller, ranked);
+  }
+
+  private static List<Integer> accountIds(List<SuggestedReviewerInfo> reviewers) {
+    return reviewers.stream()
+        .filter(r -> r.account != null)
+        .map(r -> r.account._accountId)
+        .collect(toList());
+  }
+
   private TestAccount createAccountWithSecondaryEmail(String name, String secondaryEmail)
       throws Exception {
     TestAccount foo = accountCreator.create(name(name), "foo.primary@example.com", "Foo", null);
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java
index 59a941d..95de918 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java
@@ -131,7 +131,7 @@
   private File verifySnapshot(Response<?> rsp) {
     assertThat(rsp.value()).isInstanceOf(SnapshotInfo.class);
     SnapshotInfo snapshotInfo = (SnapshotInfo) rsp.value();
-    Path snapshotDir = sitePaths.resolve("index").resolve("snapshots").resolve(snapshotInfo.id);
+    Path snapshotDir = sitePaths.index_dir.resolve("snapshots").resolve(snapshotInfo.id);
     File snapshot = snapshotDir.toFile();
     assertThat(snapshot.exists()).isTrue();
     assertThat(snapshot.isDirectory()).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
index 8311d96..4d440b9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
@@ -15,10 +15,15 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.entities.Permission.READ;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.TestAccount;
@@ -30,13 +35,26 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.util.List;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
 public class SuggestBranchReviewersIT extends AbstractDaemonTest {
 
+  /**
+   * Runs all tests with a real FanOut thread pool, so that reviewer visibility checks run
+   * concurrently. The default test configuration uses a direct executor.
+   */
+  @ConfigSuite.Config
+  public static Config fanOutThreadPool() {
+    Config cfg = new Config();
+    cfg.setInt("execution", null, "fanOutThreadPoolSize", 4);
+    return cfg;
+  }
+
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Inject private ProjectOperations projectOperations;
@@ -106,6 +124,47 @@
   }
 
   @Test
+  public void suggestReviewers_mixedVisibility_onlyVisibleAccountsUpToLimit() throws Exception {
+    String prefix = name("vis");
+    ImmutableList.Builder<TestAccount> accountsBuilder = ImmutableList.builder();
+    for (int i = 0; i < 6; i++) {
+      accountsBuilder.add(accountCreator.create(prefix + "-" + i));
+    }
+    ImmutableList<TestAccount> accounts = accountsBuilder.build();
+
+    AccountGroup.UUID readers =
+        groupOperations
+            .newGroup()
+            .name(name("branch-readers"))
+            .members(user1.id(), accounts.get(0).id(), accounts.get(2).id(), accounts.get(4).id())
+            .create();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/otherBranch").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/heads/otherBranch").group(readers))
+        .update();
+    ImmutableSet<Integer> visibleIds =
+        ImmutableSet.of(
+            accounts.get(0).id().get(), accounts.get(2).id().get(), accounts.get(4).id().get());
+
+    requestScopeOperations.setApiUser(user1.id());
+
+    List<Integer> limited = accountIds(suggestReviewers(prefix, 2));
+    assertThat(limited).hasSize(2);
+    assertThat(visibleIds).containsAtLeastElementsIn(limited);
+
+    assertThat(accountIds(suggestReviewers(prefix, 10))).containsExactlyElementsIn(visibleIds);
+  }
+
+  private static List<Integer> accountIds(List<SuggestedReviewerInfo> reviewers) {
+    return reviewers.stream()
+        .filter(r -> r.account != null)
+        .map(r -> r.account._accountId)
+        .collect(toList());
+  }
+
+  @Test
   public void suggestReviewerAsCc() throws Exception {
     requestScopeOperations.setApiUser(user.id());
 
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AuthTokenExpiryNotifierIT.java b/javatests/com/google/gerrit/acceptance/server/account/AuthTokenExpiryNotifierIT.java
new file mode 100644
index 0000000..156faab
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/AuthTokenExpiryNotifierIT.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.AuthTokenExpiryNotifier;
+import com.google.inject.Inject;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import org.junit.Test;
+
+/**
+ * Integration tests for {@link AuthTokenExpiryNotifier} using real Gerrit infrastructure.
+ *
+ * <p>These tests verify that:
+ *
+ * <ul>
+ *   <li>The notifier handles various configurations correctly
+ *   <li>The notifier runs without errors in various scenarios
+ *   <li>The integration with the email system works correctly
+ * </ul>
+ *
+ * <p>Note: Email content verification is limited because FakeEmailSender may not capture async
+ * emails reliably. The unit tests verify the notification logic, while these integration tests
+ * ensure the notifier runs without errors.
+ */
+public class AuthTokenExpiryNotifierIT extends AbstractDaemonTest {
+
+  @Inject private AuthTokenExpiryNotifier notifier;
+  @Inject private AuthTokenAccessor authTokenAccessor;
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void notifierDoesNotSendEmailsWithNoTokens() throws Exception {
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void emailSentForTokenExpiringIn21Days() throws Exception {
+    Account.Id accountId = accountCreator.user1().id();
+    Instant expiration = Instant.now().plus(21, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    @SuppressWarnings("unused")
+    var unused =
+        authTokenAccessor.addPlainToken(
+            accountId, "token21d", "plaintoken", Optional.of(expiration));
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("will expire");
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void emailSentForExpiredToken() throws Exception {
+    // Create account with token expired 12 hours ago
+    Account.Id accountId = accountCreator.user1().id();
+    Instant expiration = Instant.now().minusSeconds(12 * 60 * 60);
+    @SuppressWarnings("unused")
+    var unused =
+        authTokenAccessor.addPlainToken(
+            accountId, "tokenexpired", "plaintoken", Optional.of(expiration));
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("has expired");
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void multipleEmailsSentForMultipleTokens() throws Exception {
+    Account.Id accountId = accountCreator.user1().id();
+    Instant expiration21d = Instant.now().plus(21, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    Instant expiration14d = Instant.now().plus(14, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+
+    @SuppressWarnings("unused")
+    var token1 =
+        authTokenAccessor.addPlainToken(
+            accountId, "token21d", "plaintoken1", Optional.of(expiration21d));
+    @SuppressWarnings("unused")
+    var token2 =
+        authTokenAccessor.addPlainToken(
+            accountId, "token14d", "plaintoken2", Optional.of(expiration14d));
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).hasSize(2);
+    assertThat(sender.getMessages().get(0).body()).contains("will expire");
+    assertThat(sender.getMessages().get(1).body()).contains("will expire");
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void multipleEmailsSentForMultipleAccounts() throws Exception {
+    Account.Id accountId1 = accountCreator.user1().id();
+    Account.Id accountId2 = accountCreator.user2().id();
+    Instant expiration = Instant.now().plus(21, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+
+    @SuppressWarnings("unused")
+    var token1 =
+        authTokenAccessor.addPlainToken(
+            accountId1, "tokenA", "plaintoken1", Optional.of(expiration));
+    @SuppressWarnings("unused")
+    var token2 =
+        authTokenAccessor.addPlainToken(
+            accountId2, "tokenB", "plaintoken2", Optional.of(expiration));
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).hasSize(2);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void notifierHandlesTokensWithoutExpiration() throws Exception {
+    Account.Id accountId = accountCreator.user1().id();
+    @SuppressWarnings("unused")
+    var unused =
+        authTokenAccessor.addPlainToken(
+            accountId, "perpetual-token", "plaintoken", Optional.empty());
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.token-expiry.notifyDaysBefore",
+      values = {"21", "14", "7"})
+  public void notifierHandlesTokenExpiringFarInFuture() throws Exception {
+    Account.Id accountId = accountCreator.user1().id();
+    Instant expiration = Instant.now().plusSeconds(60 * 24 * 60 * 60);
+    @SuppressWarnings("unused")
+    var unused =
+        authTokenAccessor.addPlainToken(
+            accountId, "token60d", "plaintoken", Optional.of(expiration));
+
+    sender.clear();
+    notifier.run();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index f57a16c..8572ced 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -52,7 +52,6 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class SubmittedTogetherIT extends AbstractDaemonTest {
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/BUILD b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
index d1a64c0..f05651d 100644
--- a/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
@@ -1,7 +1,17 @@
+load("@rules_java//java:defs.bzl", "java_library")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
+java_library(
+    name = "http_test_util",
+    srcs = ["HttpTestUtil.java"],
+    deps = [
+        "//lib:servlet-api",
+    ],
+)
+
 acceptance_tests(
     srcs = glob(["*IT.java"]),
     group = "server_httpd",
     labels = ["server"],
+    deps = [":http_test_util"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
index 1dea800..62dac8f 100644
--- a/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
@@ -22,7 +22,6 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.net.InetAddress;
-import java.net.ServerSocket;
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.List;
@@ -47,7 +46,11 @@
     Config cfg = new Config();
     cfg.setString("auth", null, "logouturl", "/test-logout");
     cfg.setString("gerrit", null, "canonicalWebUrl", "https://" + LOCALHOST + ":8443/");
-    cfg.setString("httpd", null, "listenUrl", "proxy-https://" + LOCALHOST + ":" + getFreePort());
+    cfg.setString(
+        "httpd",
+        null,
+        "listenUrl",
+        "proxy-https://" + LOCALHOST + ":" + HttpTestUtil.getFreePort());
     return cfg;
   }
 
@@ -103,10 +106,4 @@
       return Optional.empty();
     }
   }
-
-  private static int getFreePort() throws IOException {
-    try (ServerSocket s = new ServerSocket(0)) {
-      return s.getLocalPort();
-    }
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/HttpTestUtil.java b/javatests/com/google/gerrit/acceptance/server/httpd/HttpTestUtil.java
new file mode 100644
index 0000000..9200c37
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/HttpTestUtil.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.httpd;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public final class HttpTestUtil {
+  static final String REMOTE_ADDRESS_HEADER = "X-Test-Remote-Address";
+
+  private HttpTestUtil() {}
+
+  static int getFreePort() throws IOException {
+    try (ServerSocket s = new ServerSocket(0)) {
+      return s.getLocalPort();
+    }
+  }
+
+  public static final class RemoteAddressFilter implements Filter {
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+        throws IOException, ServletException {
+      HttpServletResponse httpResponse = (HttpServletResponse) response;
+      httpResponse.setHeader(REMOTE_ADDRESS_HEADER, ((HttpServletRequest) request).getRemoteAddr());
+      chain.doFilter(request, response);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/JettyProxyProtocolIT.java b/javatests/com/google/gerrit/acceptance/server/httpd/JettyProxyProtocolIT.java
new file mode 100644
index 0000000..5ebbc32
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/JettyProxyProtocolIT.java
@@ -0,0 +1,306 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.GerritServer.TestHttpServerAddress;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Assume;
+import org.junit.Test;
+
+public class JettyProxyProtocolIT extends StandaloneSiteTest {
+  private static final String SERVER_ADDRESS = "127.0.0.1";
+  private static final String TRUSTED_PROXY_ADDRESS = "127.0.0.2";
+  private static final String PROXY_SOURCE_ADDRESS = "127.0.0.3";
+  private static final String UNTRUSTED_PEER_ADDRESS = "127.0.0.99";
+  private static final String FORWARDED_CLIENT_ADDRESS = "192.0.2.1";
+  private static final int PROXY_SOURCE_PORT = 12345;
+  private static final byte[] PROXY_PROTOCOL_V2_SIGNATURE = {
+    0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a
+  };
+  private static final String PROXY_PROTOCOL_HEADER_PREFIX =
+      "PROXY TCP4 " + PROXY_SOURCE_ADDRESS + " " + SERVER_ADDRESS + " " + PROXY_SOURCE_PORT + " ";
+
+  @ConfigSuite.Default
+  public static Config config() throws IOException {
+    return config(true);
+  }
+
+  @ConfigSuite.Config
+  public static Config proxyProtocolDisabledConfig() throws IOException {
+    return config(false);
+  }
+
+  private static Config config(boolean enableProxyProtocol) throws IOException {
+    Config cfg = new Config();
+    cfg.setString("auth", null, "type", "HTTP");
+    cfg.setString("auth", null, "httpHeader", "X-Remote-User");
+    cfg.setStringList(
+        "auth", null, "httpTrustedProxyNetworks", List.of(TRUSTED_PROXY_ADDRESS + "/32"));
+    cfg.setBoolean("auth", null, "trustContainerAuth", true);
+    int serverPort = HttpTestUtil.getFreePort();
+    cfg.setString("httpd", null, "listenUrl", "proxy-http://" + SERVER_ADDRESS + ":" + serverPort);
+    cfg.setBoolean("httpd", null, "enableProxyProtocol", enableProxyProtocol);
+    cfg.setStringList(
+        "httpd", null, "filterClass", List.of(HttpTestUtil.RemoteAddressFilter.class.getName()));
+    return cfg;
+  }
+
+  @Inject @TestHttpServerAddress private InetSocketAddress httpAddress;
+
+  private enum ProxyProtocolFormat {
+    V1_PROXY,
+    V2_PROXY,
+    V2_LOCAL
+  }
+
+  private static final List<ProxyProtocolFormat> PROXY_FORMATS =
+      List.of(ProxyProtocolFormat.V1_PROXY, ProxyProtocolFormat.V2_PROXY);
+
+  @Test
+  public void trustedProxyAllowsHttpHeaderAuthenticationWithProxyProtocol() throws Exception {
+    Assume.assumeTrue(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+      for (ProxyProtocolFormat format : PROXY_FORMATS) {
+        byte[] proxyRequest = createProxyRequest(format, serverPort, request);
+
+        HttpResponse proxyResponse = sendRequest(serverPort, proxyRequest, TRUSTED_PROXY_ADDRESS);
+        assertThat(proxyResponse.statusCode()).isEqualTo(200);
+        assertThat(proxyResponse.body()).contains(admin.username());
+        assertThat(proxyResponse.headers())
+            .contains(HttpTestUtil.REMOTE_ADDRESS_HEADER + ": " + PROXY_SOURCE_ADDRESS);
+      }
+    }
+  }
+
+  @Test
+  public void directHttpFromUntrustedPeerCannotUseHttpHeaderAuthentication() throws Exception {
+    Assume.assumeTrue(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+
+      HttpResponse response = sendRequest(serverPort, request, UNTRUSTED_PEER_ADDRESS);
+      assertThat(response.statusCode()).isEqualTo(403);
+    }
+  }
+
+  @Test
+  public void untrustedProxyCannotUseProxyProtocolForHttpHeaderAuthentication() throws Exception {
+    Assume.assumeTrue(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+      for (ProxyProtocolFormat format : PROXY_FORMATS) {
+        byte[] proxyRequest = createProxyRequest(format, serverPort, request);
+
+        HttpResponse response = sendRequest(serverPort, proxyRequest, UNTRUSTED_PEER_ADDRESS);
+        assertThat(response.statusCode()).isEqualTo(403);
+      }
+    }
+  }
+
+  @Test
+  public void xForwardedForTakesPrecedenceOverProxyProtocolAddress() throws Exception {
+    Assume.assumeTrue(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "X-Forwarded-For: "
+              + FORWARDED_CLIENT_ADDRESS
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+      for (ProxyProtocolFormat format : PROXY_FORMATS) {
+        byte[] proxyRequest = createProxyRequest(format, serverPort, request);
+
+        HttpResponse response = sendRequest(serverPort, proxyRequest, TRUSTED_PROXY_ADDRESS);
+        assertThat(response.statusCode()).isEqualTo(200);
+        assertThat(response.body()).contains(admin.username());
+        assertThat(response.headers())
+            .contains(HttpTestUtil.REMOTE_ADDRESS_HEADER + ": " + FORWARDED_CLIENT_ADDRESS);
+      }
+    }
+  }
+
+  // A v2 LOCAL command describes a connection made by the proxy itself, not a relayed client.
+  // The receiver must ignore the address block and keep the actual socket peer address.
+  @Test
+  public void localCommandUsesActualPeerAddress() throws Exception {
+    Assume.assumeTrue(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+      byte[] localRequest = createProxyRequest(ProxyProtocolFormat.V2_LOCAL, serverPort, request);
+
+      HttpResponse response = sendRequest(serverPort, localRequest, TRUSTED_PROXY_ADDRESS);
+      assertThat(response.statusCode()).isEqualTo(200);
+      assertThat(response.body()).contains(admin.username());
+      assertThat(response.headers())
+          .contains(HttpTestUtil.REMOTE_ADDRESS_HEADER + ": " + TRUSTED_PROXY_ADDRESS);
+      assertThat(response.headers()).doesNotContain(PROXY_SOURCE_ADDRESS);
+    }
+  }
+
+  @Test
+  public void proxyProtocolHeaderIsRejectedWhenDisabled() throws Exception {
+    Assume.assumeFalse(baseConfig.getBoolean("httpd", null, "enableProxyProtocol", false));
+
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      int serverPort = httpAddress.getPort();
+      String request =
+          "GET /a/accounts/self HTTP/1.1\r\n"
+              + "Host: localhost\r\n"
+              + "X-Remote-User: "
+              + admin.username()
+              + "\r\n"
+              + "Connection: close\r\n\r\n";
+      for (ProxyProtocolFormat format : PROXY_FORMATS) {
+        byte[] proxyRequest = createProxyRequest(format, serverPort, request);
+
+        HttpResponse response = sendRequest(serverPort, proxyRequest, TRUSTED_PROXY_ADDRESS);
+        assertThat(response.statusCode()).isEqualTo(400);
+      }
+    }
+  }
+
+  private static HttpResponse sendRequest(int port, String request, String sourceAddress)
+      throws IOException {
+    return sendRequest(port, request.getBytes(StandardCharsets.US_ASCII), sourceAddress);
+  }
+
+  private static HttpResponse sendRequest(int port, byte[] request, String sourceAddress)
+      throws IOException {
+    try (Socket socket = new Socket()) {
+      if (sourceAddress != null) {
+        socket.bind(new InetSocketAddress(sourceAddress, 0));
+      }
+      socket.connect(new InetSocketAddress(SERVER_ADDRESS, port));
+      socket.getOutputStream().write(request);
+      socket.getOutputStream().flush();
+      return parseResponse(
+          new String(socket.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
+    }
+  }
+
+  private static byte[] createProxyRequest(
+      ProxyProtocolFormat format, int targetPort, String request) throws IOException {
+    if (format == ProxyProtocolFormat.V1_PROXY) {
+      return (PROXY_PROTOCOL_HEADER_PREFIX + targetPort + "\r\n" + request)
+          .getBytes(StandardCharsets.US_ASCII);
+    }
+    byte command = format == ProxyProtocolFormat.V2_LOCAL ? (byte) 0x20 : (byte) 0x21;
+    return concat(createProxyProtocolV2Header(command, targetPort), request);
+  }
+
+  private static byte[] createProxyProtocolV2Header(byte command, int targetPort)
+      throws IOException {
+    ByteBuffer header = ByteBuffer.allocate(28);
+    header.put(PROXY_PROTOCOL_V2_SIGNATURE);
+    header.put(command); // Version 2, PROXY or LOCAL command.
+    header.put((byte) 0x11); // IPv4, stream transport.
+    header.putShort((short) 12); // IPv4 addresses and ports: 4 + 4 + 2 + 2 bytes.
+    header.put(InetAddress.getByName(PROXY_SOURCE_ADDRESS).getAddress());
+    header.put(InetAddress.getByName(SERVER_ADDRESS).getAddress());
+    header.putShort((short) PROXY_SOURCE_PORT);
+    header.putShort((short) targetPort);
+    return header.array();
+  }
+
+  private static byte[] concat(byte[] header, String request) {
+    byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
+    byte[] result = new byte[header.length + requestBytes.length];
+    System.arraycopy(header, 0, result, 0, header.length);
+    System.arraycopy(requestBytes, 0, result, header.length, requestBytes.length);
+    return result;
+  }
+
+  private static HttpResponse parseResponse(String response) throws IOException {
+    int headerEnd = response.indexOf("\r\n\r\n");
+    if (headerEnd < 0) {
+      throw new IOException("HTTP response did not contain a header terminator");
+    }
+    int statusLineEnd = response.indexOf("\r\n");
+    if (statusLineEnd < 0 || statusLineEnd > headerEnd) {
+      throw new IOException("HTTP response did not contain a status line");
+    }
+    String[] statusLineParts = response.substring(0, statusLineEnd).split("\\s+", 3);
+    if (statusLineParts.length < 2 || !statusLineParts[0].startsWith("HTTP/")) {
+      throw new IOException("Invalid HTTP status line: " + response.substring(0, statusLineEnd));
+    }
+    try {
+      return new HttpResponse(
+          Integer.parseInt(statusLineParts[1]),
+          response.substring(statusLineEnd + 2, headerEnd),
+          response.substring(headerEnd + 4));
+    } catch (NumberFormatException e) {
+      throw new IOException("Invalid HTTP status code: " + statusLineParts[1], e);
+    }
+  }
+
+  private record HttpResponse(int statusCode, String headers, String body) {}
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/BUILD b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
index f90924b..ea6255f 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
@@ -14,7 +14,10 @@
         "no_windows",
         "server",
     ],
-    deps = DEPS + [":util"],
+    deps = DEPS + [
+        ":util",
+        "//lib/commons:validator",
+    ],
 ) for f in glob(
     ["*IT.java"],
     exclude = ["AbstractMailIT.java"],
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
index 3145234..7312308 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
@@ -16,44 +16,21 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.validator.routines.DomainValidator.ArrayType.COUNTRY_CODE_RO;
+import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_RO;
+import static org.apache.commons.validator.routines.DomainValidator.ArrayType.INFRASTRUCTURE_RO;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.inject.Inject;
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.reflect.Field;
-import java.util.Locale;
-import org.junit.After;
-import org.junit.BeforeClass;
+import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.commons.validator.routines.DomainValidator.ArrayType;
 import org.junit.Test;
 
 public class EmailValidatorIT extends AbstractDaemonTest {
-  private static final String UNSUPPORTED_PREFIX = "#! ";
-
   @Inject private OutgoingEmailValidator validator;
 
-  @BeforeClass
-  public static void setUpClass() throws Exception {
-    // Reset before first use, in case other tests have already run in this JVM.
-    resetDomainValidator();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    resetDomainValidator();
-  }
-
-  private static void resetDomainValidator() throws Exception {
-    Class<?> c = Class.forName("org.apache.commons.validator.routines.DomainValidator");
-    Field f = c.getDeclaredField("inUse");
-    f.setAccessible(true);
-    f.setBoolean(c, false);
-  }
-
   @Test
   @GerritConfig(name = "sendemail.allowTLD", value = "example")
   public void testCustomTopLevelDomain() throws Exception {
@@ -72,30 +49,14 @@
 
   @Test
   public void validateTopLevelDomains() throws Exception {
-    try (InputStream in = this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) {
-      if (in == null) {
-        throw new Exception("TLD list not found");
-      }
-      BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8));
-      String tld;
-      while ((tld = r.readLine()) != null) {
-        if (tld.startsWith("# ") || tld.startsWith("XN--")) {
-          // Ignore comments and non-latin domains
-          continue;
-        }
-        if (tld.startsWith(UNSUPPORTED_PREFIX)) {
-          String test =
-              "test@example." + tld.toLowerCase(Locale.US).substring(UNSUPPORTED_PREFIX.length());
-          assertWithMessage("expected invalid TLD \"" + test + "\"")
-              .that(validator.isValid(test))
-              .isFalse();
-        } else {
-          String test = "test@example." + tld.toLowerCase(Locale.US);
-          assertWithMessage("failed to validate TLD \"" + test + "\"")
-              .that(validator.isValid(test))
-              .isTrue();
-        }
+    for (ArrayType tldType : new ArrayType[] {INFRASTRUCTURE_RO, COUNTRY_CODE_RO, GENERIC_RO}) {
+      for (String tld : DomainValidator.getTLDEntries(tldType)) {
+        String test = "test@example." + tld;
+        assertWithMessage("failed to validate TLD \"" + test + "\"")
+            .that(validator.isValid(test))
+            .isTrue();
       }
     }
+    assertThat(validator.isValid("test@example.invalid")).isFalse();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index d7f19aa..2e65ddd 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -112,12 +112,10 @@
   private static void assertHeaders(Map<String, EmailHeader> have, Map<String, Object> want)
       throws Exception {
     for (Map.Entry<String, Object> entry : want.entrySet()) {
-      if (entry.getValue() instanceof String) {
-        assertThat(have)
-            .containsEntry("X-" + entry.getKey(), new StringEmailHeader((String) entry.getValue()));
-      } else if (entry.getValue() instanceof Instant) {
-        assertThat(have)
-            .containsEntry("X-" + entry.getKey(), new EmailHeader.Date((Instant) entry.getValue()));
+      if (entry.getValue() instanceof String s) {
+        assertThat(have).containsEntry("X-" + entry.getKey(), new StringEmailHeader(s));
+      } else if (entry.getValue() instanceof Instant instant) {
+        assertThat(have).containsEntry("X-" + entry.getKey(), new EmailHeader.Date(instant));
       } else {
         throw new Exception(
             "Object has unsupported type: "
@@ -132,13 +130,13 @@
     for (Map.Entry<String, Object> entry : want.entrySet()) {
       if (entry.getValue() instanceof String) {
         assertThat(body).contains(entry.getKey() + ": " + entry.getValue());
-      } else if (entry.getValue() instanceof Instant) {
+      } else if (entry.getValue() instanceof Instant instant) {
         assertThat(body)
             .contains(
                 entry.getKey()
                     + ": "
                     + MailProcessingUtil.rfcDateformatter.format(
-                        ZonedDateTime.ofInstant((Instant) entry.getValue(), ZoneId.of("UTC"))));
+                        ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))));
       } else {
         throw new Exception(
             "Object has unsupported type: "
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 3cc88f9..239c2e71 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -307,8 +307,8 @@
   }
 
   private void ensureAtomicTransactions(Repository repo) throws Exception {
-    if (repo instanceof InMemoryRepository) {
-      ((InMemoryRepository) repo).setPerformsAtomicTransactions(true);
+    if (repo instanceof InMemoryRepository inMemoryRepository) {
+      inMemoryRepository.setPerformsAtomicTransactions(true);
     } else {
       assertWithMessage("performsAtomicTransactions on %s", repo)
           .that(repo.getRefDatabase().performsAtomicTransactions())
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ConvertRefStorageIT.java b/javatests/com/google/gerrit/acceptance/ssh/ConvertRefStorageIT.java
new file mode 100644
index 0000000..dadbc69
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/ConvertRefStorageIT.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class ConvertRefStorageIT extends AbstractDaemonTest {
+  @Test
+  @GerritConfig(name = "core.usePerRequestRefCache", value = "true")
+  public void refusesConversionToReftableWhenPerRequestRefCacheEnabled() throws Exception {
+    adminSshSession.exec(
+        "gerrit convert-ref-storage --project " + project.get() + " --format reftable");
+    adminSshSession.assertFailure(
+        "Converting to reftable requires core.usePerRequestRefCache=false");
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
index 4b7519a..448897c 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.index.testing.AbstractFakeIndex;
 import com.google.gerrit.index.testing.FakeIndexVersionManager;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.AbstractIndexModule;
-import com.google.gerrit.server.index.IndexDir;
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeIndex;
@@ -36,7 +36,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.assistedinject.Assisted;
-import java.nio.file.Path;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
@@ -107,11 +106,11 @@
 
   @com.google.inject.Inject
   CustomModuleFakeIndexChange(
-      @IndexDir Path indexDir,
+      SitePaths sitePaths,
       ChangeData.Factory changeDataFactory,
       @Assisted Schema<ChangeData> schema,
       @GerritServerConfig Config cfg,
       IndexConfig indexConfig) {
-    super(indexDir, changeDataFactory, schema, cfg, indexConfig);
+    super(sitePaths, changeDataFactory, schema, cfg, indexConfig);
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
index a89d75d..69b6388 100644
--- a/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
@@ -212,8 +212,8 @@
         return null;
       }
       Type genericType = field.getGenericType();
-      if (genericType instanceof ParameterizedType) {
-        return (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
+      if (genericType instanceof ParameterizedType parameterizedType) {
+        return (Class<?>) parameterizedType.getActualTypeArguments()[0];
       }
       return null;
     }
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index f8e7d80..49963a2 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -642,8 +642,8 @@
       return null;
     }
     Type genericType = field.getGenericType();
-    if (genericType instanceof ParameterizedType) {
-      return (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
+    if (genericType instanceof ParameterizedType parameterizedType) {
+      return (Class<?>) parameterizedType.getActualTypeArguments()[0];
     }
     return null;
   }
diff --git a/javatests/com/google/gerrit/httpd/raw/DocServletTest.java b/javatests/com/google/gerrit/httpd/raw/DocServletTest.java
index 2f09aa2..f90fb2f 100644
--- a/javatests/com/google/gerrit/httpd/raw/DocServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/DocServletTest.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION;
-import static org.mockito.Mockito.when;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
 import java.io.IOException;
@@ -34,31 +31,20 @@
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
 
 @RunWith(JUnit4.class)
 public class DocServletTest {
 
-  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
-  @Mock private ExperimentFeatures experimentFeatures;
   private FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
   private DocServlet docServlet;
 
   @Before
   public void setUp() throws Exception {
-    when(experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION))
-        .thenReturn(true);
-
     docServlet =
-        new DocServlet(
-            CacheBuilder.newBuilder().maximumSize(1).build(), false, experimentFeatures) {
+        new DocServlet(CacheBuilder.newBuilder().maximumSize(1).build(), false) {
           private static final long serialVersionUID = 1L;
 
           @Override
@@ -85,19 +71,6 @@
   }
 
   @Test
-  public void experimentDisabled_unchangedResponse() throws Exception {
-    when(experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION))
-        .thenReturn(false);
-    FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH);
-    request.setAttribute("nonce", NONCE);
-    FakeHttpServletResponse response = new FakeHttpServletResponse();
-
-    docServlet.doGet(request, response);
-
-    assertThat(response.getActualBody()).isEqualTo(HTML_RESPONSE.getBytes(StandardCharsets.UTF_8));
-  }
-
-  @Test
   public void nonHtmlResponse_unchangedResponse() throws Exception {
     FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(NON_HTML_FILE_PATH);
     request.setAttribute("nonce", NONCE);
diff --git a/javatests/com/google/gerrit/pgm/http/jetty/JettyServerTest.java b/javatests/com/google/gerrit/pgm/http/jetty/JettyServerTest.java
new file mode 100644
index 0000000..c84096c
--- /dev/null
+++ b/javatests/com/google/gerrit/pgm/http/jetty/JettyServerTest.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.http.jetty;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.ProxyConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.Test;
+
+public class JettyServerTest {
+  @Test
+  public void proxyProtocolConnectorUsesProxyConnectionFactory() {
+    HttpConfiguration config = new HttpConfiguration();
+
+    ServerConnector connector = JettyServer.newServerConnector(new Server(), 0, 0, config, true);
+    ImmutableList<ConnectionFactory> factories =
+        ImmutableList.copyOf(connector.getConnectionFactories());
+
+    assertThat(factories).hasSize(2);
+    assertThat(factories.get(0)).isInstanceOf(ProxyConnectionFactory.class);
+    assertThat(factories.get(1)).isInstanceOf(HttpConnectionFactory.class);
+  }
+
+  @Test
+  public void regularConnectorDoesNotUseProxyProtocol() {
+    HttpConfiguration config = new HttpConfiguration();
+
+    ServerConnector connector = JettyServer.newServerConnector(new Server(), 0, 0, config, false);
+    ImmutableList<ConnectionFactory> factories =
+        ImmutableList.copyOf(connector.getConnectionFactories());
+
+    assertThat(factories).hasSize(1);
+    assertThat(factories.get(0)).isInstanceOf(HttpConnectionFactory.class);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfigTest.java b/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfigTest.java
new file mode 100644
index 0000000..e363eb5
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationConfigTest.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.AuthTokenExpiryNotificationConfig.AUTH;
+import static com.google.gerrit.server.account.AuthTokenExpiryNotificationConfig.NOTIFY;
+import static com.google.gerrit.server.account.AuthTokenExpiryNotificationConfig.NOTIFY_DAYS_BEFORE;
+import static com.google.gerrit.server.account.AuthTokenExpiryNotificationConfig.TOKEN_EXPIRY;
+import static org.junit.Assert.assertThrows;
+
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class AuthTokenExpiryNotificationConfigTest {
+
+  @Test
+  public void defaultNotificationDays() throws Exception {
+    Config config = new Config();
+    AuthTokenExpiryNotificationConfig notificationConfig =
+        new AuthTokenExpiryNotificationConfig(config);
+
+    assertThat(notificationConfig.getNotificationDays()).containsExactly(0, 7, 14, 21);
+  }
+
+  @Test
+  public void readsNotificationDaysFromConfig() throws Exception {
+    Config config = new Config();
+    config.setStringList(AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE, List.of("5", "10", "15"));
+    AuthTokenExpiryNotificationConfig notificationConfig =
+        new AuthTokenExpiryNotificationConfig(config);
+
+    assertThat(notificationConfig.getNotificationDays()).containsExactly(5, 10, 15);
+  }
+
+  @Test
+  public void invalidNotificationDaysThrows() throws Exception {
+    Config config = new Config();
+    config.setStringList(AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE, List.of("1", "b", "c"));
+
+    assertThrows(ConfigInvalidException.class, () -> new AuthTokenExpiryNotificationConfig(config));
+  }
+
+  @Test
+  public void featureIsEnabledByDefault() throws Exception {
+    Config config = new Config();
+    AuthTokenExpiryNotificationConfig notificationConfig =
+        new AuthTokenExpiryNotificationConfig(config);
+
+    assertThat(notificationConfig.isEnabled()).isTrue();
+  }
+
+  @Test
+  public void featureCanBeDisabled() throws Exception {
+    Config config = new Config();
+    config.setBoolean(AUTH, TOKEN_EXPIRY, NOTIFY, false);
+    AuthTokenExpiryNotificationConfig notificationConfig =
+        new AuthTokenExpiryNotificationConfig(config);
+
+    assertThat(notificationConfig.isEnabled()).isFalse();
+  }
+
+  @Test
+  public void negativeValuesAreIgnored() throws Exception {
+    Config config = new Config();
+    config.setStringList(AUTH, TOKEN_EXPIRY, NOTIFY_DAYS_BEFORE, List.of("-5", "5", "10", "15"));
+    AuthTokenExpiryNotificationConfig notificationConfig =
+        new AuthTokenExpiryNotificationConfig(config);
+    assertThat(notificationConfig.getNotificationDays()).containsExactly(5, 10, 15);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationTest.java b/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationTest.java
new file mode 100644
index 0000000..38319f3
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AuthTokenExpiryNotificationTest.java
@@ -0,0 +1,317 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.mail.EmailFactories;
+import com.google.gerrit.server.mail.send.OutgoingEmail;
+import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AuthTokenExpiryNotificationTest {
+
+  @Mock private Accounts accounts;
+  @Mock private AuthTokenAccessor tokenAccessor;
+  @Mock private EmailFactories emailFactories;
+  @Mock private OutgoingEmail outgoingEmail;
+  @Mock private EmailDecorator emailDecorator;
+
+  private AuthTokenExpiryNotificationConfig defaultConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    defaultConfig = new AuthTokenExpiryNotificationConfig(new Config());
+  }
+
+  @Test
+  public void suggestsNotificationOnConfiguredDays() {
+    AuthTokenExpiryNotifier notifier = new AuthTokenExpiryNotifier(null, null, null, defaultConfig);
+
+    Instant checkTime = Instant.now();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(1, ChronoUnit.HOURS))).isTrue();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(7, ChronoUnit.DAYS))).isTrue();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(14, ChronoUnit.DAYS))).isTrue();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(21, ChronoUnit.DAYS))).isTrue();
+  }
+
+  @Test
+  public void doesNotSuggestNotificationForNonScheduledDays() {
+    AuthTokenExpiryNotifier notifier = new AuthTokenExpiryNotifier(null, null, null, defaultConfig);
+
+    Instant checkTime = Instant.now();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(3, ChronoUnit.DAYS))).isFalse();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.plus(30, ChronoUnit.DAYS))).isFalse();
+  }
+
+  @Test
+  public void doesNotSuggestNotificationWhenExpired() {
+    AuthTokenExpiryNotifier notifier = new AuthTokenExpiryNotifier(null, null, null, defaultConfig);
+
+    Instant checkTime = Instant.now();
+    assertThat(notifier.shouldBeNotified(checkTime, checkTime.minus(3, ChronoUnit.DAYS))).isFalse();
+  }
+
+  @Test
+  public void shouldNotifyExpiredReturnsTrueWithinOneDayWindow() {
+    Instant checkTime = Instant.now();
+    assertThat(
+            AuthTokenExpiryNotifier.shouldNotifyExpired(
+                checkTime, checkTime.minus(1, ChronoUnit.MINUTES)))
+        .isTrue();
+    assertThat(
+            AuthTokenExpiryNotifier.shouldNotifyExpired(
+                checkTime, checkTime.minus(23, ChronoUnit.HOURS)))
+        .isTrue();
+  }
+
+  @Test
+  public void shouldNotifyExpiredReturnsFalseOutsideOneDayWindow() {
+    Instant checkTime = Instant.now();
+    assertThat(
+            AuthTokenExpiryNotifier.shouldNotifyExpired(
+                checkTime, checkTime.minus(25, ChronoUnit.HOURS)))
+        .isFalse();
+  }
+
+  @Test
+  public void shouldNotifyExpiredReturnsFalseForFutureExpiry() {
+    Instant checkTime = Instant.now();
+    assertThat(
+            AuthTokenExpiryNotifier.shouldNotifyExpired(
+                checkTime, checkTime.plus(1, ChronoUnit.HOURS)))
+        .isFalse();
+  }
+
+  @Test
+  public void runSkipsAllWhenDisabled() throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        AuthTokenExpiryNotificationConfig.AUTH,
+        AuthTokenExpiryNotificationConfig.TOKEN_EXPIRY,
+        AuthTokenExpiryNotificationConfig.NOTIFY,
+        false);
+    AuthTokenExpiryNotificationConfig disabledConfig = new AuthTokenExpiryNotificationConfig(cfg);
+
+    AuthTokenExpiryNotifier notifier =
+        new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, disabledConfig);
+    notifier.run();
+
+    verify(accounts, never()).all();
+    verify(tokenAccessor, never()).getTokens(any(Account.Id.class));
+    verify(emailFactories, never()).createOutgoingEmail(any(), any());
+  }
+
+  @Test
+  public void runSendsEmailForExpiringToken() throws Exception {
+    Account account = buildAccount(1001, "Alice");
+    AccountState accountState = AccountState.forAccount(account);
+
+    Instant expiry = Instant.now().plus(7, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    AuthToken token = AuthToken.create("token-alice", "hashed", Optional.of(expiry));
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of(token));
+    when(emailFactories.createAuthTokenWillExpireEmail(account, token)).thenReturn(emailDecorator);
+    when(emailFactories.createOutgoingEmail(
+            eq(EmailFactories.AUTH_TOKEN_WILL_EXPIRE), eq(emailDecorator)))
+        .thenReturn(outgoingEmail);
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(outgoingEmail, times(1)).sendAsync();
+  }
+
+  @Test
+  public void runDoesNotSendEmailWhenExpiryNotOnScheduledDay() throws Exception {
+    Account account = buildAccount(1002, "Bob");
+    AccountState accountState = AccountState.forAccount(account);
+
+    // 5 days away — not in the default schedule [0, 7, 14, 21]
+    Instant expiry = Instant.now().plus(5, ChronoUnit.DAYS);
+    AuthToken token = AuthToken.create("token-bob", "hashed", Optional.of(expiry));
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of(token));
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(emailFactories, never()).createOutgoingEmail(any(), any());
+  }
+
+  @Test
+  public void runSkipsTokensWithoutExpirationDate() throws Exception {
+    Account account = buildAccount(1003, "Carol");
+    AccountState accountState = AccountState.forAccount(account);
+
+    AuthToken token = AuthToken.create("token-carol", "hashed", Optional.empty());
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of(token));
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(emailFactories, never()).createOutgoingEmail(any(), any());
+  }
+
+  @Test
+  public void runSendsExpiryEmailForRecentlyExpiredToken() throws Exception {
+    Account account = buildAccount(1004, "Dave");
+    AccountState accountState = AccountState.forAccount(account);
+
+    // Expired 1 hour ago — within the 24-hour expiry-notification window
+    Instant expiry = Instant.now().minus(1, ChronoUnit.HOURS);
+    AuthToken token = AuthToken.create("token-dave", "hashed", Optional.of(expiry));
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of(token));
+    when(emailFactories.createAuthTokenExpiredEmail(account, token)).thenReturn(emailDecorator);
+    when(emailFactories.createOutgoingEmail(
+            eq(EmailFactories.AUTH_TOKEN_EXPIRED), eq(emailDecorator)))
+        .thenReturn(outgoingEmail);
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(outgoingEmail, times(1)).sendAsync();
+  }
+
+  @Test
+  public void runSkipsTokenExpiredMoreThanOneDayAgo() throws Exception {
+    Account account = buildAccount(1011, "Eve");
+    AccountState accountState = AccountState.forAccount(account);
+
+    // Expired 25 hours ago — outside the expiry-notification window
+    Instant expiry = Instant.now().minus(25, ChronoUnit.HOURS);
+    AuthToken token = AuthToken.create("token-eve", "hashed", Optional.of(expiry));
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of(token));
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(emailFactories, never()).createOutgoingEmail(any(), any());
+  }
+
+  @Test
+  public void runHandlesMultipleAccountsAndTokens() throws Exception {
+    Account alice = buildAccount(1005, "Alice");
+    Account bob = buildAccount(1006, "Bob");
+    AccountState aliceState = AccountState.forAccount(alice);
+    AccountState bobState = AccountState.forAccount(bob);
+
+    // Alice has two tokens: one on-schedule (+7d+1h bucket), one off-schedule (+5d)
+    Instant aliceExpiryScheduled = Instant.now().plus(7, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    Instant aliceExpiryOff = Instant.now().plus(5, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    AuthToken aliceToken1 =
+        AuthToken.create("alice-token-1", "hashed", Optional.of(aliceExpiryScheduled));
+    AuthToken aliceToken2 =
+        AuthToken.create("alice-token-2", "hashed", Optional.of(aliceExpiryOff));
+
+    // Bob has one on-schedule token
+    Instant bobExpiry = Instant.now().plus(14, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    AuthToken bobToken = AuthToken.create("bob-token", "hashed", Optional.of(bobExpiry));
+
+    EmailDecorator aliceDecorator = mock(EmailDecorator.class);
+    EmailDecorator bobDecorator = mock(EmailDecorator.class);
+    OutgoingEmail aliceEmail = mock(OutgoingEmail.class);
+    OutgoingEmail bobEmail = mock(OutgoingEmail.class);
+
+    when(accounts.all()).thenReturn(List.of(aliceState, bobState));
+    when(tokenAccessor.getTokens(alice.id())).thenReturn(List.of(aliceToken1, aliceToken2));
+    when(tokenAccessor.getTokens(bob.id())).thenReturn(List.of(bobToken));
+
+    when(emailFactories.createAuthTokenWillExpireEmail(alice, aliceToken1))
+        .thenReturn(aliceDecorator);
+    when(emailFactories.createOutgoingEmail(
+            eq(EmailFactories.AUTH_TOKEN_WILL_EXPIRE), eq(aliceDecorator)))
+        .thenReturn(aliceEmail);
+
+    when(emailFactories.createAuthTokenWillExpireEmail(bob, bobToken)).thenReturn(bobDecorator);
+    when(emailFactories.createOutgoingEmail(
+            eq(EmailFactories.AUTH_TOKEN_WILL_EXPIRE), eq(bobDecorator)))
+        .thenReturn(bobEmail);
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(aliceEmail, times(1)).sendAsync();
+    verify(bobEmail, times(1)).sendAsync();
+    // aliceToken2 is off-schedule — no email
+    verify(emailFactories, never()).createAuthTokenWillExpireEmail(alice, aliceToken2);
+  }
+
+  @Test
+  public void runHandlesAccountWithNoTokens() throws Exception {
+    Account account = buildAccount(1009, "Empty");
+    AccountState accountState = AccountState.forAccount(account);
+
+    when(accounts.all()).thenReturn(List.of(accountState));
+    when(tokenAccessor.getTokens(account.id())).thenReturn(List.of());
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(emailFactories, never()).createOutgoingEmail(any(), any());
+  }
+
+  @Test
+  public void runDoesNotThrowWhenTokenAccessorFails() throws Exception {
+    Account account = buildAccount(1010, "Failing");
+    Account bob = buildAccount(1011, "Bob");
+    AccountState accountState = AccountState.forAccount(account);
+    AccountState bobState = AccountState.forAccount(bob);
+
+    Instant expiry = Instant.now().plus(7, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    AuthToken bobToken = AuthToken.create("bob-token", "hashed", Optional.of(expiry));
+
+    EmailDecorator bobDecorator = mock(EmailDecorator.class);
+    OutgoingEmail bobEmail = mock(OutgoingEmail.class);
+
+    when(accounts.all()).thenReturn(List.of(accountState, bobState));
+    when(tokenAccessor.getTokens(account.id()))
+        .thenThrow(new ConfigInvalidException("corrupt config"));
+    when(tokenAccessor.getTokens(bob.id())).thenReturn(List.of(bobToken));
+
+    when(emailFactories.createAuthTokenWillExpireEmail(bob, bobToken)).thenReturn(bobDecorator);
+    when(emailFactories.createOutgoingEmail(
+            eq(EmailFactories.AUTH_TOKEN_WILL_EXPIRE), eq(bobDecorator)))
+        .thenReturn(bobEmail);
+
+    new AuthTokenExpiryNotifier(accounts, tokenAccessor, emailFactories, defaultConfig).run();
+
+    verify(bobEmail, times(1)).sendAsync();
+  }
+
+  private static Account buildAccount(int id, String fullName) {
+    return Account.builder(Account.id(id), Instant.EPOCH).setFullName(fullName).build();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
index 03e4278..f1fead1 100644
--- a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -48,6 +49,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -67,20 +69,30 @@
 
   private ExternalIdFactoryNoteDbImpl externalIdFactory;
   @Mock private AuthConfig authConfig;
+  private Repository allUsersRepository;
 
   @Before
   public void setUp() throws Exception {
     externalIdFactory =
         new ExternalIdFactoryNoteDbImpl(new ExternalIdKeyFactory(() -> false), authConfig);
     externalIdCache = CacheBuilder.newBuilder().build();
-    repoManager.createRepository(ALL_USERS).close();
+    allUsersRepository = repoManager.createRepository(ALL_USERS);
     externalIdReader =
         new ExternalIdReader(
-            repoManager, ALL_USERS, new DisabledMetricMaker(), externalIdFactory, authConfig);
+            Providers.of(allUsersRepository),
+            ALL_USERS,
+            new DisabledMetricMaker(),
+            externalIdFactory,
+            authConfig);
     externalIdReaderSpy = Mockito.spy(externalIdReader);
     loader = createLoader();
   }
 
+  @After
+  public void tearDown() throws Exception {
+    allUsersRepository.close();
+  }
+
   @Test
   public void worksOnSingleCommit() throws Exception {
     ObjectId firstState = insertExternalId(1, 1);
@@ -250,8 +262,7 @@
 
   private ExternalIdCacheLoader createLoader() {
     return new ExternalIdCacheLoader(
-        repoManager,
-        ALL_USERS,
+        Providers.of(allUsersRepository),
         externalIdReaderSpy,
         externalIdCache,
         new DisabledMetricMaker(),
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
index 9f55711..e06c564 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
@@ -66,7 +66,7 @@
 
     assertThat(readField(store, "url"))
         .isEqualTo(
-            "jdbc:h2:file:"
+            "jdbc:h2:async:"
                 + cacheDir.resolve("demo-cache-v2").toAbsolutePath()
                 + ";DB_CLOSE_DELAY=-1");
   }
diff --git a/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java b/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java
index 7e1a23b..270b179 100644
--- a/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java
@@ -20,13 +20,20 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.RawInputUtil;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -37,7 +44,7 @@
 
 public class TreeCreatorTest {
 
-  private Repository repository;
+  private InMemoryRepository repository;
   private TestRepository<?> testRepository;
 
   @Before
@@ -66,6 +73,79 @@
   }
 
   @Test
+  public void multipleFileContentModificationsCreateSinglePackfile() throws Exception {
+    TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+    treeCreator.addTreeModifications(
+        ImmutableList.of(
+            new ChangeFileContentModification("file1.txt", RawInputUtil.create("Content 1")),
+            new ChangeFileContentModification("file2.txt", RawInputUtil.create("Content 2")),
+            new ChangeFileContentModification("dir/file3.txt", RawInputUtil.create("Content 3"))));
+    ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+    assertThat(getFileContent(newTreeId, "file1.txt")).isEqualTo("Content 1");
+    assertThat(getFileContent(newTreeId, "file2.txt")).isEqualTo("Content 2");
+    assertThat(getFileContent(newTreeId, "dir/file3.txt")).isEqualTo("Content 3");
+    assertThat(repository.getObjectDatabase().getPacks()).hasLength(1);
+  }
+
+  @Test
+  public void multipleFileContentModificationsReuseProvidedObjectInserter() throws Exception {
+    RevCommit baseCommit = testRepository.branch("refs/heads/main").commit().create();
+    int initialPacks = repository.getObjectDatabase().getPacks().length;
+
+    try (ObjectInserter oi = repository.newObjectInserter();
+        ObjectReader or = oi.newReader()) {
+      TreeCreator treeCreator = TreeCreator.basedOn(baseCommit, oi, or);
+      treeCreator.addTreeModifications(
+          ImmutableList.of(
+              new ChangeFileContentModification("file1.txt", RawInputUtil.create("Content 1")),
+              new ChangeFileContentModification("file2.txt", RawInputUtil.create("Content 2"))));
+      ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+      assertThat(getFileContent(newTreeId, "file1.txt")).isEqualTo("Content 1");
+      assertThat(getFileContent(newTreeId, "file2.txt")).isEqualTo("Content 2");
+      assertThat(repository.getObjectDatabase().getPacks()).hasLength(initialPacks + 1);
+    }
+  }
+
+  @Test
+  public void customTreeModificationImplementingOnlyThreeArgMethodWorks() throws Exception {
+    TreeModification customModification =
+        new TreeModification() {
+          @Override
+          public ImmutableList<DirCacheEditor.PathEdit> getPathEdits(
+              Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents)
+              throws IOException {
+            try (ObjectInserter inserter = repository.newObjectInserter()) {
+              ObjectId blobId = inserter.insert(OBJ_BLOB, "Legacy Custom".getBytes(UTF_8));
+              inserter.flush();
+              return ImmutableList.of(new AddPath("custom.txt", FileMode.REGULAR_FILE, blobId));
+            }
+          }
+
+          @Override
+          public ImmutableSet<String> getFilePaths() {
+            return ImmutableSet.of("custom.txt");
+          }
+        };
+
+    TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+    treeCreator.addTreeModifications(ImmutableList.of(customModification));
+    ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+    assertThat(getFileContent(newTreeId, "custom.txt")).isEqualTo("Legacy Custom");
+  }
+
+  @Test
+  public void changeFileContentModificationWorksViaThreeArgGetPathEdits() throws Exception {
+    ChangeFileContentModification modification =
+        new ChangeFileContentModification("direct.txt", RawInputUtil.create("Direct 3-arg"));
+    List<DirCacheEditor.PathEdit> edits =
+        modification.getPathEdits(repository, ObjectId.zeroId(), ImmutableList.of());
+    assertThat(edits).hasSize(1);
+  }
+
+  @Test
   public void renameFileModificationDoesNotComplainAboutEmptyTree() throws Exception {
     TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
     treeCreator.addTreeModifications(
diff --git a/javatests/com/google/gerrit/server/git/AllUsersRepositoryProviderTest.java b/javatests/com/google/gerrit/server/git/AllUsersRepositoryProviderTest.java
index df78f7b..bac09cb 100644
--- a/javatests/com/google/gerrit/server/git/AllUsersRepositoryProviderTest.java
+++ b/javatests/com/google/gerrit/server/git/AllUsersRepositoryProviderTest.java
@@ -107,7 +107,7 @@
 
   private static class TrackingRepositoryManager extends LocalDiskRepositoryManager {
     private TrackingRepository openedRepository;
-    private Consumer<TrackingRepository> onOpen = repo -> {};
+    private volatile Consumer<TrackingRepository> onOpen = repo -> {};
 
     TrackingRepositoryManager(SitePaths site, Config cfg) {
       super(site, cfg);
diff --git a/javatests/com/google/gerrit/server/git/validators/CommitValidatorsTest.java b/javatests/com/google/gerrit/server/git/validators/CommitValidatorsTest.java
index 4477f23..3cfab6c 100644
--- a/javatests/com/google/gerrit/server/git/validators/CommitValidatorsTest.java
+++ b/javatests/com/google/gerrit/server/git/validators/CommitValidatorsTest.java
@@ -41,10 +41,10 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectNotifyFilterValidator;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ReceiveCommitControl;
 import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 import org.mockito.ArgumentMatchers;
@@ -101,7 +101,7 @@
     PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners =
         mock(PluginSetContext.class);
     return new CommitValidators.Factory(
-        new PersonIdent("gerrit", "gerrit@example.com"),
+        mock(ReceiveCommitControl.class),
         urlFormatter,
         new Config(),
         pluginValidators,
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 8d79fad..ae4437f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -424,8 +424,8 @@
   }
 
   private RevCommit parseCommit(ObjectId id) throws Exception {
-    if (id instanceof RevCommit) {
-      return (RevCommit) id;
+    if (id instanceof RevCommit revCommit) {
+      return revCommit;
     }
     try (RevWalk walk = new RevWalk(repo)) {
       RevCommit commit = walk.parseCommit(id);
diff --git a/javatests/com/google/gerrit/server/restapi/change/VisibilityCheckPipelineTest.java b/javatests/com/google/gerrit/server/restapi/change/VisibilityCheckPipelineTest.java
new file mode 100644
index 0000000..1b2fd3c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/restapi/change/VisibilityCheckPipelineTest.java
@@ -0,0 +1,337 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.entities.Account;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntPredicate;
+import java.util.stream.IntStream;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class VisibilityCheckPipelineTest {
+  private ExecutorService pool;
+  private ExecutorService directExecutor;
+  private ExecutorService callerExecutor;
+
+  @Before
+  public void setUp() {
+    pool = Executors.newFixedThreadPool(25);
+    directExecutor = MoreExecutors.newDirectExecutorService();
+    callerExecutor = Executors.newSingleThreadExecutor();
+  }
+
+  @After
+  public void tearDown() {
+    pool.shutdownNow();
+    directExecutor.shutdownNow();
+    callerExecutor.shutdownNow();
+  }
+
+  /** Both executors Gerrit can use: the FanOut pool and a direct executor (pool size 0). */
+  private ImmutableList<ExecutorService> executors() {
+    return ImmutableList.of(pool, directExecutor);
+  }
+
+  @Test
+  public void returnsVisibleCandidatesInRankOrder() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> id % 3 == 0);
+      List<Account.Id> result = drain(pipeline(firstIds(20), check, executor, 4));
+      assertThat(result).containsExactlyElementsIn(ids(0, 3, 6, 9)).inOrder();
+    }
+  }
+
+  @Test
+  public void eachCandidateCheckedAtMostOnce() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> id % 3 == 0);
+      var unused = drain(pipeline(firstIds(20), check, executor, 3));
+      assertThat(check.duplicateChecks()).isEmpty();
+
+      FakeCheck noneVisible = new FakeCheck(id -> false);
+      var unused2 = drain(pipeline(firstIds(10), noneVisible, executor, 3));
+      assertThat(noneVisible.duplicateChecks()).isEmpty();
+    }
+  }
+
+  @Test
+  public void outOfOrderCompletionPreservesOrder() throws Exception {
+    FakeCheck check = new FakeCheck(id -> true);
+    CountDownLatch release0 = check.block(0);
+    VisibilityCheckPipeline pipeline = pipeline(firstIds(10), check, pool, 3);
+
+    Future<List<Account.Id>> result = callerExecutor.submit(() -> drain(pipeline));
+    check.awaitCompleted(1);
+    check.awaitCompleted(2);
+    assertThat(result.isDone()).isFalse();
+
+    release0.countDown();
+    assertThat(result.get(10, SECONDS)).containsExactlyElementsIn(ids(0, 1, 2)).inOrder();
+  }
+
+  @Test
+  public void stopsAtLimit() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> true);
+      List<Account.Id> result = drain(pipeline(firstIds(10), check, executor, 3));
+      assertThat(result).containsExactlyElementsIn(ids(0, 1, 2)).inOrder();
+      // Initial window is limit + 2; visible results never start replacements.
+      assertThat(check.totalChecks()).isAtMost(5);
+    }
+  }
+
+  @Test
+  public void invisibleCandidateStartsReplacement() throws Exception {
+    FakeCheck check = new FakeCheck(id -> false);
+    check.setDelayMillis(5);
+    List<Account.Id> result = drain(pipeline(firstIds(20), check, pool, 3));
+    assertThat(result).isEmpty();
+    assertThat(check.checkedIds()).containsExactlyElementsIn(firstIds(20));
+    assertThat(check.maxConcurrent()).isAtMost(5);
+  }
+
+  @Test
+  public void slowCandidateDoesNotBlockRefill() throws Exception {
+    // Candidate 0 is slow and visible, all others are quickly not visible.
+    FakeCheck check = new FakeCheck(id -> id == 0);
+    CountDownLatch release0 = check.block(0);
+    VisibilityCheckPipeline pipeline = pipeline(firstIds(10), check, pool, 2);
+
+    Future<List<Account.Id>> result = callerExecutor.submit(() -> drain(pipeline));
+    // The initial window is 0..3. Candidate 9 is only started by refills triggered by
+    // invisible results, which must happen while candidate 0 is still blocked.
+    check.awaitCompleted(9);
+    assertThat(result.isDone()).isFalse();
+
+    release0.countDown();
+    assertThat(result.get(10, SECONDS)).containsExactlyElementsIn(ids(0));
+  }
+
+  @Test
+  public void checkThrows_treatedAsInvisible() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> true);
+      check.throwFor(1);
+      List<Account.Id> result = drain(pipeline(firstIds(10), check, executor, 3));
+      assertThat(result).containsExactlyElementsIn(ids(0, 2, 3)).inOrder();
+    }
+  }
+
+  @Test
+  public void getAfterStop_returnsNull() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> true);
+      VisibilityCheckPipeline pipeline = pipeline(firstIds(10), check, executor, 3);
+      pipeline.stop();
+      assertThat(pipeline.get()).isNull();
+      assertThat(check.totalChecks()).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void emptyCandidates() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> true);
+      assertThat(drain(pipeline(ImmutableList.of(), check, executor, 3))).isEmpty();
+      assertThat(check.totalChecks()).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void zeroLimit() throws Exception {
+    for (ExecutorService executor : executors()) {
+      FakeCheck check = new FakeCheck(id -> true);
+      assertThat(drain(pipeline(firstIds(10), check, executor, 0))).isEmpty();
+      assertThat(check.totalChecks()).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void interruptedCaller_propagates() throws Exception {
+    FakeCheck check = new FakeCheck(id -> true);
+    CountDownLatch release0 = check.block(0);
+    try {
+      VisibilityCheckPipeline pipeline = pipeline(firstIds(10), check, pool, 3);
+      AtomicInteger interrupted = new AtomicInteger();
+      Thread caller =
+          new Thread(
+              () -> {
+                try {
+                  var unused = pipeline.get();
+                } catch (InterruptedException e) {
+                  interrupted.incrementAndGet();
+                }
+              });
+      caller.start();
+      check.awaitStarted(0);
+      caller.interrupt();
+      caller.join(10_000);
+      assertThat(caller.isAlive()).isFalse();
+      assertThat(interrupted.get()).isEqualTo(1);
+    } finally {
+      release0.countDown();
+    }
+  }
+
+  @Test
+  public void interruptedCaller_getThrows() throws Exception {
+    FakeCheck check = new FakeCheck(id -> true);
+    CountDownLatch release0 = check.block(0);
+    try {
+      VisibilityCheckPipeline pipeline = pipeline(firstIds(10), check, pool, 3);
+      Thread.currentThread().interrupt();
+      assertThrows(InterruptedException.class, pipeline::get);
+    } finally {
+      Thread.interrupted();
+      release0.countDown();
+    }
+  }
+
+  private static VisibilityCheckPipeline pipeline(
+      List<Account.Id> candidates, FakeCheck check, ExecutorService executor, int limit) {
+    return new VisibilityCheckPipeline(candidates, check, executor, limit);
+  }
+
+  private static List<Account.Id> drain(VisibilityCheckPipeline pipeline)
+      throws InterruptedException {
+    List<Account.Id> result = new ArrayList<>();
+    try {
+      Account.Id id;
+      while ((id = pipeline.get()) != null) {
+        result.add(id);
+      }
+    } finally {
+      pipeline.stop();
+    }
+    return result;
+  }
+
+  private static ImmutableList<Account.Id> firstIds(int count) {
+    return IntStream.range(0, count).mapToObj(Account::id).collect(toImmutableList());
+  }
+
+  private static ImmutableList<Account.Id> ids(int... values) {
+    return IntStream.of(values).mapToObj(Account::id).collect(toImmutableList());
+  }
+
+  /** Records calls and lets tests block or fail checks for particular candidates. */
+  private static class FakeCheck implements VisibilityCheckPipeline.CandidateCheck {
+    private final IntPredicate visible;
+    private final Map<Integer, AtomicInteger> calls = new ConcurrentHashMap<>();
+    private final Map<Integer, CountDownLatch> blockers = new ConcurrentHashMap<>();
+    private final Map<Integer, CountDownLatch> started = new ConcurrentHashMap<>();
+    private final Map<Integer, CountDownLatch> completed = new ConcurrentHashMap<>();
+    private final AtomicInteger running = new AtomicInteger();
+    private final AtomicInteger maxRunning = new AtomicInteger();
+    private volatile ImmutableSet<Integer> throwFor = ImmutableSet.of();
+    private volatile long delayMillis;
+
+    FakeCheck(IntPredicate visible) {
+      this.visible = visible;
+    }
+
+    CountDownLatch block(int id) {
+      CountDownLatch latch = new CountDownLatch(1);
+      blockers.put(id, latch);
+      return latch;
+    }
+
+    void throwFor(int... ids) {
+      throwFor = IntStream.of(ids).boxed().collect(ImmutableSet.toImmutableSet());
+    }
+
+    void setDelayMillis(long delayMillis) {
+      this.delayMillis = delayMillis;
+    }
+
+    @Override
+    public boolean isVisible(Account.Id accountId) throws Exception {
+      int id = accountId.get();
+      calls.computeIfAbsent(id, k -> new AtomicInteger()).incrementAndGet();
+      latch(started, id).countDown();
+      int now = running.incrementAndGet();
+      maxRunning.accumulateAndGet(now, Math::max);
+      try {
+        CountDownLatch blocker = blockers.get(id);
+        if (blocker != null) {
+          blocker.await();
+        }
+        if (delayMillis > 0) {
+          Thread.sleep(delayMillis);
+        }
+        if (throwFor.contains(id)) {
+          throw new IllegalStateException("check failed for " + id);
+        }
+        return visible.test(id);
+      } finally {
+        running.decrementAndGet();
+        latch(completed, id).countDown();
+      }
+    }
+
+    void awaitStarted(int id) throws InterruptedException {
+      assertThat(latch(started, id).await(10, SECONDS)).isTrue();
+    }
+
+    void awaitCompleted(int id) throws InterruptedException {
+      assertThat(latch(completed, id).await(10, SECONDS)).isTrue();
+    }
+
+    int totalChecks() {
+      return calls.values().stream().mapToInt(AtomicInteger::get).sum();
+    }
+
+    ImmutableList<Account.Id> checkedIds() {
+      return calls.keySet().stream().sorted().map(Account::id).collect(toImmutableList());
+    }
+
+    ImmutableList<Integer> duplicateChecks() {
+      return calls.entrySet().stream()
+          .filter(e -> e.getValue().get() > 1)
+          .map(Map.Entry::getKey)
+          .sorted()
+          .collect(toImmutableList());
+    }
+
+    int maxConcurrent() {
+      return maxRunning.get();
+    }
+
+    private static CountDownLatch latch(Map<Integer, CountDownLatch> latches, int id) {
+      return latches.computeIfAbsent(id, k -> new CountDownLatch(1));
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreIT.java b/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreIT.java
index bac1b31..ef4624a 100644
--- a/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreIT.java
+++ b/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreIT.java
@@ -82,6 +82,26 @@
   }
 
   @Test
+  public void legacyFileUrlStartsAndStoresReviewedFlag() throws Exception {
+    SitePaths sitePaths = new SitePaths(temporaryFolder.getRoot().toPath());
+    Config cfg = new Config();
+    cfg.setString(
+        JdbcAccountPatchReviewStore.ACCOUNT_PATCH_REVIEW_DB,
+        null,
+        "url",
+        "jdbc:h2:file:"
+            + temporaryFolder.newFolder("legacy-account-patch-reviews").toPath().resolve("db"));
+    H2JGitLockAccountPatchReviewStore legacyStore =
+        new H2JGitLockAccountPatchReviewStore(cfg, sitePaths);
+    legacyStore.start();
+
+    var unused = legacyStore.markReviewed(PS, ACCOUNT, FILE);
+
+    assertThat(legacyStore.findReviewed(PS, ACCOUNT)).isPresent();
+    assertThat(legacyStore.findReviewed(PS, ACCOUNT).get().files()).containsExactly(FILE);
+  }
+
+  @Test
   public void concurrentMarksAreAllDurable() throws Exception {
     int nThreads = 8;
     int filesPerThread = 5;
diff --git a/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreTest.java b/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreTest.java
index c15d1c8..33b2ad6 100644
--- a/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreTest.java
+++ b/javatests/com/google/gerrit/server/schema/H2JGitLockAccountPatchReviewStoreTest.java
@@ -24,18 +24,23 @@
 public class H2JGitLockAccountPatchReviewStoreTest {
   @Test
   public void lockTargetFromUrl_plainPath() {
+    assertThat(lockTargetFromUrl("jdbc:h2:async:/path/to/db")).isEqualTo(new File("/path/to/db"));
+  }
+
+  @Test
+  public void lockTargetFromUrl_legacyFileUrl() {
     assertThat(lockTargetFromUrl("jdbc:h2:file:/path/to/db")).isEqualTo(new File("/path/to/db"));
   }
 
   @Test
   public void lockTargetFromUrl_stripsOptions() {
-    assertThat(lockTargetFromUrl("jdbc:h2:file:/path/to/db;FILE_LOCK=NO;DB_CLOSE_DELAY=0"))
+    assertThat(lockTargetFromUrl("jdbc:h2:async:/path/to/db;FILE_LOCK=NO;DB_CLOSE_DELAY=0"))
         .isEqualTo(new File("/path/to/db"));
   }
 
   @Test
   public void lockTargetFromUrl_unescapesSemicolonInPath() {
-    assertThat(lockTargetFromUrl("jdbc:h2:file:/path/with\\;semi/db;FILE_LOCK=NO"))
+    assertThat(lockTargetFromUrl("jdbc:h2:async:/path/with\\;semi/db;FILE_LOCK=NO"))
         .isEqualTo(new File("/path/with;semi/db"));
   }
 
diff --git a/javatests/com/google/gerrit/server/schema/JdbcAccountPatchReviewStoreTest.java b/javatests/com/google/gerrit/server/schema/JdbcAccountPatchReviewStoreTest.java
index d8c876b..18b9479 100644
--- a/javatests/com/google/gerrit/server/schema/JdbcAccountPatchReviewStoreTest.java
+++ b/javatests/com/google/gerrit/server/schema/JdbcAccountPatchReviewStoreTest.java
@@ -16,53 +16,118 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.server.config.SitePaths;
 import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class JdbcAccountPatchReviewStoreTest {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
   @Test
   public void checkCreateH2Url() {
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("test").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("test").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("test.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("test.db").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db.mv.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("test.db.mv.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("test.db.mv.db").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db.mv.db.trace.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("test.db.mv.db.trace.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("test.db.mv.db.trace.db").toAbsolutePath());
   }
 
   @Test
   public void checkCreateH2UrlWithPath() {
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("path/to/test")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("path/to/test").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("path/to/test").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("path/to/test.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("path/to/test.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("path/to/test.db").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("path/to/test.db.mv.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("path/to/test.db.mv.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("path/to/test.db.mv.db").toAbsolutePath());
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("path/to/test.db.mv.db.trace.db")))
-        .isEqualTo("jdbc:h2:file:" + Path.of("path/to/test.db.mv.db.trace.db").toAbsolutePath());
+        .isEqualTo("jdbc:h2:async:" + Path.of("path/to/test.db.mv.db.trace.db").toAbsolutePath());
   }
 
   @Test
   public void checkCreateH2UrlWithSemicolon() {
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test;test")))
         .isEqualTo(
-            "jdbc:h2:file:" + Path.of("test;test").toAbsolutePath().toString().replace(";", "\\;"));
+            "jdbc:h2:async:"
+                + Path.of("test;test").toAbsolutePath().toString().replace(";", "\\;"));
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db;test")))
         .isEqualTo(
-            "jdbc:h2:file:"
+            "jdbc:h2:async:"
                 + Path.of("test.db;test").toAbsolutePath().toString().replace(";", "\\;"));
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db.mv.db;test")))
         .isEqualTo(
-            "jdbc:h2:file:"
+            "jdbc:h2:async:"
                 + Path.of("test.db.mv.db;test").toAbsolutePath().toString().replace(";", "\\;"));
     assertThat(JdbcAccountPatchReviewStore.createH2Url(Path.of("test.db.mv.db.trace.db;test")))
         .isEqualTo(
-            "jdbc:h2:file:"
+            "jdbc:h2:async:"
                 + Path.of("test.db.mv.db.trace.db;test")
                     .toAbsolutePath()
                     .toString()
                     .replace(";", "\\;"));
   }
+
+  @Test
+  public void normalizeH2Url_preservesAsyncUrl() {
+    assertThat(JdbcAccountPatchReviewStore.normalizeH2Url("jdbc:h2:async:/path/to/db;FILE_LOCK=NO"))
+        .isEqualTo("jdbc:h2:async:/path/to/db;FILE_LOCK=NO");
+  }
+
+  @Test
+  public void normalizeH2Url_convertsLegacyFileUrlToAsyncUrl() {
+    assertThat(JdbcAccountPatchReviewStore.normalizeH2Url("jdbc:h2:file:/path/to/db;FILE_LOCK=NO"))
+        .isEqualTo("jdbc:h2:async:/path/to/db;FILE_LOCK=NO");
+  }
+
+  @Test
+  public void getUrl_normalizesLegacyFileUrlFromConfig() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        JdbcAccountPatchReviewStore.ACCOUNT_PATCH_REVIEW_DB,
+        null,
+        "url",
+        "jdbc:h2:file:/path/to/db;FILE_LOCK=NO");
+
+    assertThat(
+            JdbcAccountPatchReviewStore.getUrl(
+                cfg, new SitePaths(temporaryFolder.getRoot().toPath())))
+        .isEqualTo("jdbc:h2:async:/path/to/db;FILE_LOCK=NO");
+  }
+
+  @Test
+  public void normalizeH2Url_preservesNonFileH2Url() {
+    assertThat(JdbcAccountPatchReviewStore.normalizeH2Url("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"))
+        .isEqualTo("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
+  }
+
+  @Test
+  public void createH2Url_canCreateAndReadAsyncH2Database() throws Exception {
+    String url =
+        JdbcAccountPatchReviewStore.createH2Url(
+            temporaryFolder.newFolder("db").toPath().resolve("account_patch_reviews"));
+
+    try (Connection con = DriverManager.getConnection(url);
+        Statement stmt = con.createStatement()) {
+      stmt.executeUpdate("CREATE TABLE account_patch_reviews (id INT PRIMARY KEY, path VARCHAR)");
+      stmt.executeUpdate("INSERT INTO account_patch_reviews VALUES (1, 'README.md')");
+    }
+
+    try (Connection con = DriverManager.getConnection(url);
+        Statement stmt = con.createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT path FROM account_patch_reviews WHERE id = 1")) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(1)).isEqualTo("README.md");
+      assertThat(rs.next()).isFalse();
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/server/util/H2UrlTest.java b/javatests/com/google/gerrit/server/util/H2UrlTest.java
new file mode 100644
index 0000000..e2e0ac4
--- /dev/null
+++ b/javatests/com/google/gerrit/server/util/H2UrlTest.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class H2UrlTest {
+  @Test
+  public void prefixes() {
+    assertThat(H2Url.ASYNC_PREFIX).isEqualTo("jdbc:h2:async:");
+    assertThat(H2Url.FILE_PREFIX).isEqualTo("jdbc:h2:file:");
+  }
+}
diff --git a/javatests/com/google/gerrit/util/crypto/BUILD b/javatests/com/google/gerrit/util/crypto/BUILD
index 7ea0626..5de0855 100644
--- a/javatests/com/google/gerrit/util/crypto/BUILD
+++ b/javatests/com/google/gerrit/util/crypto/BUILD
@@ -6,7 +6,6 @@
     deps = [
         "//java/com/google/gerrit/util/crypto",
         "//lib:junit",
-        "//lib/bouncycastle:bcprov",
         "//lib/truth",
     ],
 )
diff --git a/modules/gitiles b/modules/gitiles
index e5135e3..2c8ddbd 160000
--- a/modules/gitiles
+++ b/modules/gitiles
@@ -1 +1 @@
-Subproject commit e5135e39eac0f5f3b91c7616f9131be32286a136
+Subproject commit 2c8ddbd3a2829ed2082535f8778be9d4709d0f61
diff --git a/plugins/BUILD b/plugins/BUILD
index 2df9bfc..1f5b43d 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -75,6 +75,7 @@
     "//java/com/google/gerrit/server/util/time",
     "//java/com/google/gerrit/proto",
     "//java/com/google/gerrit/util/cli",
+    "//java/com/google/gerrit/util/crypto",
     "//java/com/google/gerrit/util/http",
     "//java/com/google/gerrit/util/logging",
     "//lib/antlr:java-runtime",
diff --git a/plugins/delete-project b/plugins/delete-project
index 3603583..51d5aba 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 3603583484c45e578aeff66885723dfd171932fa
+Subproject commit 51d5aba8a2248a99b77c061fd5ea1526f6257b84
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 35ac0fd..dcd5914 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 35ac0fdbd5d4661dfe2b85d4a9eeecd1f2a79080
+Subproject commit dcd591412d3d6169ee2deb60d4b0fcee50f7d1df
diff --git a/plugins/replication b/plugins/replication
index 23d3866..f64dcc5 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 23d386651175ca83d214793749852339684d3c2c
+Subproject commit f64dcc5fc696d10ffea212f31849b11cefc990ba
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements-template-dialog.ts b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements-template-dialog.ts
index 38c4380..746efa1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements-template-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements-template-dialog.ts
@@ -3,10 +3,7 @@
  * Copyright 2025 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {RepoName, SubmitRequirementInfo} from '../../../types/common';
-import {firePageError} from '../../../utils/event-util';
-import {getAppContext} from '../../../services/app-context';
-import {ErrorCallback} from '../../../api/rest';
+import {SubmitRequirementInfo} from '../../../types/common';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
@@ -21,23 +18,15 @@
 
 @customElement('gr-repo-submit-requirements-template-dialog')
 export class GrRepoSubmitRequirementsTemplateDialog extends LitElement {
-  @property({type: String})
-  repo?: RepoName;
-
   @query('#templateDialog')
   private readonly templateDialog?: HTMLDialogElement;
 
-  @state()
+  @property({type: Array})
   templates?: SubmitRequirementInfo[];
 
   @state()
-  loading = false;
-
-  @state()
   selectedTemplate?: SubmitRequirementInfo;
 
-  private readonly restApiService = getAppContext().restApiService;
-
   static override get styles() {
     return [
       materialStyles,
@@ -102,11 +91,6 @@
           font-family: monospace;
           color: var(--secondary-text-color);
         }
-        .loading {
-          text-align: center;
-          padding: var(--spacing-l);
-          color: var(--deemphasized-text-color);
-        }
         .no-templates {
           padding: var(--spacing-l);
           text-align: center;
@@ -124,73 +108,64 @@
           <div class="main" slot="main">
             <div class="gr-form-styles">
               ${when(
-                this.loading,
-                () => html`<div class="loading">Loading templates...</div>`,
-                () =>
-                  html`${when(
-                    !this.templates || this.templates.length === 0,
-                    () => html`<div class="no-templates">
-                      No templates available
-                    </div>`,
-                    () => html`<div class="template-list">
-                      ${this.templates!.map(
-                        template => html`
-                          <div
-                            class="template-item ${this.selectedTemplate
-                              ?.name === template.name
-                              ? 'selected'
-                              : ''}"
-                            @click=${() => this.selectTemplate(template)}
-                          >
-                            <div class="template-item-name">
-                              ${template.name}
-                            </div>
-                            ${when(
-                              template.description,
-                              () => html`
-                                <div class="template-field">
-                                  <span class="field-label">Description:</span>
-                                  <span class="field-value"
-                                    >${template.description}</span
-                                  >
-                                </div>
-                              `
-                            )}
+                !this.templates || this.templates.length === 0,
+                () => html`<div class="no-templates">
+                  No templates available
+                </div>`,
+                () => html`<div class="template-list">
+                  ${this.templates!.map(
+                    template => html`
+                      <div
+                        class="template-item ${this.selectedTemplate?.name ===
+                        template.name
+                          ? 'selected'
+                          : ''}"
+                        @click=${() => this.selectTemplate(template)}
+                      >
+                        <div class="template-item-name">${template.name}</div>
+                        ${when(
+                          template.description,
+                          () => html`
                             <div class="template-field">
-                              <span class="field-label">Submittability:</span>
-                              <span class="field-value monospace"
-                                >${template.submittability_expression}</span
+                              <span class="field-label">Description:</span>
+                              <span class="field-value"
+                                >${template.description}</span
                               >
                             </div>
-                            ${when(
-                              template.applicability_expression,
-                              () => html`
-                                <div class="template-field">
-                                  <span class="field-label"
-                                    >Applicability:</span
-                                  >
-                                  <span class="field-value monospace"
-                                    >${template.applicability_expression}</span
-                                  >
-                                </div>
-                              `
-                            )}
-                            ${when(
-                              template.override_expression,
-                              () => html`
-                                <div class="template-field">
-                                  <span class="field-label">Override:</span>
-                                  <span class="field-value monospace"
-                                    >${template.override_expression}</span
-                                  >
-                                </div>
-                              `
-                            )}
-                          </div>
-                        `
-                      )}
-                    </div>`
-                  )}`
+                          `
+                        )}
+                        <div class="template-field">
+                          <span class="field-label">Submittability:</span>
+                          <span class="field-value monospace"
+                            >${template.submittability_expression}</span
+                          >
+                        </div>
+                        ${when(
+                          template.applicability_expression,
+                          () => html`
+                            <div class="template-field">
+                              <span class="field-label">Applicability:</span>
+                              <span class="field-value monospace"
+                                >${template.applicability_expression}</span
+                              >
+                            </div>
+                          `
+                        )}
+                        ${when(
+                          template.override_expression,
+                          () => html`
+                            <div class="template-field">
+                              <span class="field-label">Override:</span>
+                              <span class="field-value monospace"
+                                >${template.override_expression}</span
+                              >
+                            </div>
+                          `
+                        )}
+                      </div>
+                    `
+                  )}
+                </div>`
               )}
             </div>
           </div>
@@ -210,39 +185,10 @@
     `;
   }
 
-  async show() {
-    this.loading = true;
+  show() {
     this.selectedTemplate = undefined;
-    this.templates = undefined;
-
     assertIsDefined(this.templateDialog, 'templateDialog');
     this.templateDialog.showModal();
-
-    await this.fetchTemplates();
-  }
-
-  private async fetchTemplates() {
-    if (!this.repo) {
-      return;
-    }
-
-    try {
-      const errFn: ErrorCallback = response => {
-        firePageError(response);
-      };
-
-      const templates =
-        await this.restApiService.getRepoSubmitRequirementTemplates(
-          this.repo,
-          errFn
-        );
-      this.templates = templates || [];
-    } catch (e) {
-      console.error('Failed to fetch templates:', e);
-      this.templates = [];
-    } finally {
-      this.loading = false;
-    }
   }
 
   private selectTemplate(template: SubmitRequirementInfo) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
index 88859d0..ccd70c5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
@@ -89,6 +89,8 @@
   @state()
   isEditing = false;
 
+  @state() submitRequirementTemplates?: SubmitRequirementInfo[];
+
   private readonly getNavigation = resolve(this, navigationToken);
 
   private readonly restApiService = getAppContext().restApiService;
@@ -195,6 +197,24 @@
     }
   }
 
+  private async fetchTemplates() {
+    if (!this.repo) return;
+    try {
+      const errFn: ErrorCallback = response => {
+        firePageError(response);
+      };
+      const templates =
+        await this.restApiService.getRepoSubmitRequirementTemplates(
+          this.repo,
+          errFn
+        );
+      this.submitRequirementTemplates = templates ?? [];
+    } catch (e) {
+      console.error('Failed to fetch submit requirement templates:', e);
+      this.submitRequirementTemplates = [];
+    }
+  }
+
   override render() {
     return html`
       <gr-list-view
@@ -287,7 +307,7 @@
 
       ${this.renderCreateDialog()} ${this.renderDeleteDialog()}
       <gr-repo-submit-requirements-template-dialog
-        .repo=${this.repo}
+        .templates=${this.submitRequirementTemplates}
         @template-selected=${(
           e: CustomEvent<{template: SubmitRequirementInfo}>
         ) => this.handleTemplateSelected(e.detail.template)}
@@ -299,6 +319,7 @@
     if (changedProperties.has('repo')) {
       this.getSubmitRequirements();
       this.checkProjectOwner();
+      this.fetchTemplates();
     }
   }
 
@@ -360,7 +381,7 @@
   private handleSelectFromTemplateClick() {
     assertIsDefined(this.templateDialog, 'templateDialog');
     this.createDialog?.close();
-    void this.templateDialog.show();
+    this.templateDialog.show();
   }
 
   private handleTemplateSelected(template: SubmitRequirementInfo) {
@@ -468,7 +489,8 @@
           </div>
           <div class="main" slot="main">
             ${when(
-              !this.isEditing,
+              !this.isEditing &&
+                (this.submitRequirementTemplates?.length ?? 0) > 0,
               () => html`
                 <div class="template-action-row">
                   <gr-button
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
index 4de1789..dec430a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
@@ -36,6 +36,9 @@
           },
         ] as SubmitRequirementInfo[])
       );
+      stubRestApi('getRepoSubmitRequirementTemplates').returns(
+        Promise.resolve([] as SubmitRequirementInfo[])
+      );
       element.repo = 'test' as RepoName;
     });
 
@@ -181,17 +184,6 @@
             <gr-dialog>
               <div class="header" slot="header">Create Submit Requirement</div>
               <div class="main" slot="main">
-                <div class="template-action-row">
-                  <gr-button
-                    aria-disabled="false"
-                    class="action select-from-template"
-                    link=""
-                    role="button"
-                    tabindex="0"
-                  >
-                    Select from Template
-                  </gr-button>
-                </div>
                 <div class="gr-form-styles">
               <div id="form">
                 <section>
@@ -395,6 +387,49 @@
       );
     });
 
+    test('select from template button is shown when templates exist', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      element.submitRequirementTemplates = [
+        {name: 'Template-Verified'} as SubmitRequirementInfo,
+      ];
+      await element.updateComplete;
+
+      // Open the create dialog
+      const createDialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      createDialog.showModal();
+      await element.updateComplete;
+
+      const selectFromTemplateBtn = queryAndAssert<GrButton>(
+        element,
+        '.select-from-template'
+      );
+      assert.isOk(selectFromTemplateBtn);
+    });
+
+    test('select from template button is hidden when no templates exist', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      // submitRequirementTemplates is empty by default (no templates returned in setup)
+      await element.updateComplete;
+
+      // Open the create dialog
+      const createDialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      createDialog.showModal();
+      await element.updateComplete;
+
+      const selectFromTemplateBtn = element.shadowRoot?.querySelector(
+        '.select-from-template'
+      );
+      assert.isNull(selectFromTemplateBtn);
+    });
+
     test('template-selected event pre-populates create form', async () => {
       await waitEventLoop();
       element.isProjectOwner = true;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
index 6ab744a..e736be9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
@@ -19,6 +19,7 @@
 } from '../../../api/rest-api';
 import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
 import {
+  computeLabelShortcut,
   extractAssociatedLabels,
   getAllUniqueApprovals,
   getRequirements,
@@ -39,6 +40,9 @@
   @property()
   labelName?: string;
 
+  @property({type: Boolean})
+  compact = false;
+
   static override get styles() {
     return [
       submitRequirementsStyles,
@@ -49,6 +53,23 @@
           align-items: center;
           justify-content: center;
         }
+        .badge {
+          display: inline-flex;
+          align-items: center;
+          border: 1px solid var(--border-color);
+          border-radius: 12px;
+          padding: 0 6px;
+          height: 20px;
+          font-size: var(--font-size-small);
+          background: var(--table-header-background-color);
+          white-space: nowrap;
+        }
+        .badge gr-icon {
+          width: 16px;
+          height: 16px;
+          font-size: 16px;
+          margin-left: 2px;
+        }
         .container.not-applicable {
           background-color: var(--table-header-background-color);
           height: calc(var(--line-height-normal) + var(--spacing-m));
@@ -58,9 +79,19 @@
   }
 
   override render() {
+    if (
+      this.compact &&
+      this.getRequirement(this.labelName ?? '').length === 0 &&
+      !this.isTriggerVote(this.labelName ?? '')
+    )
+      return;
     return html`<div
       class="container ${this.computeClass()}"
-      title=${ifDefined(this.computeLabelTitle())}
+      title=${ifDefined(
+        this.compact
+          ? `${this.labelName}: ${this.computeLabelTitle() ?? ''}`
+          : this.computeLabelTitle()
+      )}
     >
       ${this.renderContent()}
     </div>`;
@@ -91,16 +122,28 @@
       if (votes.length > 0) {
         const bestVote = votes[0];
         return html`<gr-vote-chip
+          .labelName=${this.compact
+            ? computeLabelShortcut(this.labelName)
+            : undefined}
           .vote=${bestVote}
           .label=${labelInfo}
           tooltip-with-who-voted
         ></gr-vote-chip>`;
       }
     }
-    if (isQuickLabelInfo(labelInfo)) {
-      return html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`;
+    if (isQuickLabelInfo(labelInfo) && (!this.compact || hasVotes(labelInfo))) {
+      return html`<gr-vote-chip
+        .labelName=${this.compact
+          ? computeLabelShortcut(this.labelName)
+          : undefined}
+        .label=${labelInfo}
+      ></gr-vote-chip>`;
     }
-    return;
+    return this.compact
+      ? html`<span class="badge"
+          >${computeLabelShortcut(this.labelName)}—</span
+        >`
+      : undefined;
   }
 
   private renderUnsatisfiedState(requirement: SubmitRequirementResultInfo) {
@@ -130,6 +173,9 @@
       return this.renderStatusIcon(requirement.status);
     } else {
       return html`<gr-vote-chip
+        .labelName=${this.compact && this.labelName
+          ? computeLabelShortcut(this.labelName)
+          : undefined}
         .vote=${worstVote}
         .label=${labelInfo}
         tooltip-with-who-voted
@@ -139,17 +185,22 @@
 
   private renderStatusIcon(status: SubmitRequirementStatus) {
     const icon = iconForStatus(status);
-    return html`
+    const content = html`
       <gr-icon
         class=${icon.icon}
         icon=${icon.icon}
         ?filled=${!!icon.filled}
       ></gr-icon>
     `;
+    return this.compact
+      ? html`<span class="badge"
+          >${computeLabelShortcut(this.labelName ?? '')}${content}</span
+        >`
+      : content;
   }
 
   private computeClass(): string {
-    if (!this.labelName) return '';
+    if (!this.labelName || this.compact) return '';
     const requirements = this.getRequirement(this.labelName);
     if (requirements.length === 0 && !this.isTriggerVote(this.labelName)) {
       return 'not-applicable';
@@ -162,7 +213,10 @@
     const requirements = this.getRequirement(this.labelName);
     if (requirements.length === 0) {
       if (this.isTriggerVote(this.labelName)) {
-        return;
+        return this.compact &&
+          !hasVotes(this.change?.labels?.[this.labelName] ?? {})
+          ? 'No votes'
+          : undefined;
       } else {
         return 'Requirement not applicable';
       }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
index 88fbbff..21a7022 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
@@ -8,6 +8,7 @@
 import {html} from 'lit';
 import './gr-change-list-column-requirement';
 import {GrChangeListColumnRequirement} from './gr-change-list-column-requirement';
+import {GrVoteChip} from '../../shared/gr-vote-chip/gr-vote-chip';
 import {
   createChange,
   createNonApplicableSubmitRequirementResultInfo,
@@ -63,6 +64,31 @@
     );
   });
 
+  test('compact status badge includes the label and full tooltip', async () => {
+    element =
+      await fixture<GrChangeListColumnRequirement>(html`<gr-change-list-column-requirement
+        .change=${change}
+        labelName="Code-Review"
+        compact
+      ></gr-change-list-column-requirement>`);
+    assert.equal(
+      element.shadowRoot!.querySelector('.badge')!.textContent?.trim(),
+      'CR'
+    );
+    assert.equal(
+      element.shadowRoot!.querySelector('gr-icon')!.getAttribute('icon'),
+      'check_circle'
+    );
+    assert.equal(
+      element.shadowRoot!.querySelector('.container')!.getAttribute('title'),
+      'Code-Review: Satisfied'
+    );
+    element.labelName = 'Missing';
+    await element.updateComplete;
+    assert.isNull(element.shadowRoot!.querySelector('.badge'));
+    assert.isNull(element.shadowRoot!.querySelector('.container'));
+  });
+
   test('show worst vote when state is not satisfied', async () => {
     const VALUES_2 = {
       '-2': 'blocking',
@@ -105,7 +131,7 @@
         <gr-vote-chip tooltip-with-who-voted=""></gr-vote-chip>
       </div>`
     );
-    const voteChip = queryAndAssert(element, 'gr-vote-chip');
+    const voteChip = queryAndAssert<GrVoteChip>(element, 'gr-vote-chip');
     assert.shadowDom.equal(
       voteChip,
       /* HTML */
@@ -154,7 +180,7 @@
         <gr-vote-chip tooltip-with-who-voted=""></gr-vote-chip>
       </div>`
     );
-    const voteChip = queryAndAssert(element, 'gr-vote-chip');
+    const voteChip = queryAndAssert<GrVoteChip>(element, 'gr-vote-chip');
     assert.shadowDom.equal(
       voteChip,
       /* HTML */
@@ -166,5 +192,26 @@
         <div class="positive vote-chip">+1</div>
       </gr-tooltip-content>`
     );
+    element.compact = true;
+    await element.updateComplete;
+    await voteChip.updateComplete;
+    assert.equal(voteChip.labelName, 'CQ');
+    assert.equal(
+      voteChip.shadowRoot!.querySelector('.vote-chip')!.textContent?.trim(),
+      'CQ+1'
+    );
+    element.change = {
+      ...change,
+      labels: {'Commit-Queue': {values: VALUES_2, all: []}},
+    };
+    await element.updateComplete;
+    assert.equal(
+      element.shadowRoot!.querySelector('.badge')!.textContent?.trim(),
+      'CQ—'
+    );
+    assert.equal(
+      element.shadowRoot!.querySelector('.container')!.getAttribute('title'),
+      'Commit-Queue: No votes'
+    );
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 4fb209f..8972b8e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -48,6 +48,7 @@
 import {formStyles} from '../../../styles/form-styles';
 import '@material/web/checkbox/checkbox';
 import {materialStyles} from '../../../styles/gr-material-styles';
+import {getChangeListLabels} from '../../../utils/label-util';
 
 enum ChangeSize {
   XS = 10,
@@ -363,6 +364,7 @@
             display: flex;
           }
           .change-header,
+          .change-subject,
           .change-metadata {
             display: flex;
             align-items: center;
@@ -371,17 +373,26 @@
             gap: var(--spacing-m);
           }
           .change-header {
-            align-items: baseline;
+            color: var(--deemphasized-text-color);
+            font-size: var(--font-size-small);
+            font-weight: var(--font-weight-normal);
+            line-height: var(--line-height-small);
           }
           .change-metadata {
             flex-wrap: wrap;
+            column-gap: var(--spacing-l);
           }
           .cell.star,
           .cell.number {
             flex: none;
           }
-          .cell.number {
-            order: 1;
+          .owner,
+          .reviewers {
+            font-size: var(--font-size-small);
+            font-weight: var(--font-weight-normal);
+            line-height: var(--line-height-small);
+            --account-label-text-color: var(--deemphasized-text-color);
+            --account-label-attention-font-weight: var(--font-weight-medium);
           }
           .cell.subject {
             flex: 1;
@@ -409,12 +420,15 @@
             min-width: 0;
             max-width: 100%;
           }
-          .repo {
-            flex: 1 0 6rem;
+          .repo,
+          .cell.branch {
+            display: flex;
+            flex: 0 1 auto;
             min-width: 0;
             max-width: 100%;
           }
-          .repo a {
+          .repo a,
+          .branch a {
             white-space: normal;
             overflow-wrap: anywhere;
           }
@@ -430,8 +444,8 @@
             margin-left: auto;
             max-width: 100%;
             gap: var(--spacing-s);
+            flex-wrap: wrap;
           }
-          /* Keep absent labels in their slots, so columns stay aligned. */
           .cell.label.narrowHidden {
             display: none;
           }
@@ -440,8 +454,8 @@
             justify-content: center;
           }
           .cell.label {
-            flex: 0 0 24px;
-            width: 24px;
+            flex: 0 0 auto;
+            width: auto;
             min-height: 24px;
           }
         }
@@ -449,6 +463,13 @@
     ];
   }
 
+  private get applicableLabels() {
+    const applicableLabels = getChangeListLabels(this.change);
+    return (this.labelNames ?? []).filter(name =>
+      applicableLabels.includes(name)
+    );
+  }
+
   override render() {
     const changeUrl = this.computeChangeURL();
     // Keep the desktop table cells as direct children, as before.
@@ -475,24 +496,24 @@
       ${this.renderCellSelectionBox()}
       <div class="change-header">
         ${this.renderCellStar()} ${this.renderCellNumber(changeUrl)}
-        ${this.renderCellSubject(changeUrl)}
+        ${this.renderCellRepo()} ${this.renderCellBranch()}
       </div>
+      <div class="change-subject">${this.renderCellSubject(changeUrl)}</div>
       <div class="change-metadata">
         ${this.renderCellOwner()} ${this.renderCellReviewers()}
-        ${this.renderCellRepo()} ${this.renderCellBranch()}
         ${this.renderCellHashtags()} ${this.renderCellUpdated()}
         ${this.renderCellSubmitted()} ${this.renderCellWaiting()}
         ${this.renderCellSize()} ${this.renderCellRequirements()}
         <div class="cell votes">
-          ${this.labelNames?.map(labelName =>
+          ${this.applicableLabels.map(labelName =>
             this.renderChangeLabels(labelName)
           )}
           ${this.narrowLabels &&
-          (this.labelNames?.length ?? 0) > this.narrowLabels.length
+          this.applicableLabels.length > this.narrowLabels.length
             ? html`<td
                 class="cell label labelOverflow"
-                title=${this.labelNames
-                  ?.filter(name => !this.narrowLabels!.includes(name))
+                title=${this.applicableLabels
+                  .filter(name => !this.narrowLabels!.includes(name))
                   .join(', ') ?? ''}
                 style="order: ${this.narrowLabels.length}"
               >
@@ -536,7 +557,7 @@
   }
 
   private renderCellNumber(changeUrl: string) {
-    if (!this.showNumber) return;
+    if (!this.showNumber && !this.narrowScreen) return;
 
     return html`
       <td class="cell number">
@@ -644,7 +665,7 @@
     return html`
       <td class="cell branch">
         <a href=${this.computeRepoBranchURL()}> ${this.change?.branch} </a>
-        ${this.renderChangeBranch()}
+        ${this.narrowScreen ? '' : this.renderChangeBranch()}
       </td>
     `;
   }
@@ -770,6 +791,7 @@
     >
       <gr-change-list-column-requirement
         .change=${this.change}
+        .compact=${this.narrowScreen}
         .labelName=${labelName}
       >
       </gr-change-list-column-requirement>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index 6f209cf..35e7f34 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -429,6 +429,51 @@
     assert.equal(setUrlStub.lastCall.firstArg, '/c/a/test/repo/+/42');
   });
 
+  test('narrow rows show the number and respect hidden repo and branch', async () => {
+    await setViewport({width: 390, height: 900});
+    await waitUntil(() => !!query(element, '.change-header'));
+    element.change = change;
+    element.showNumber = false;
+    element.visibleChangeTableColumns = [
+      ColumnNames.SUBJECT,
+      ColumnNames.OWNER,
+    ];
+    await element.updateComplete;
+
+    assert.equal(
+      queryAndAssert(element, '.change-header .number').textContent?.trim(),
+      '42'
+    );
+    assert.isNotOk(query(element, '.repo'));
+    assert.isNotOk(query(element, '.branch'));
+    assert.isOk(query(element, '.change-subject .subject'));
+    assert.isOk(query(element, '.change-metadata .owner'));
+    assert.isOk(query(element, '.change-metadata .votes'));
+
+    element.visibleChangeTableColumns = [
+      ColumnNames.SUBJECT,
+      ColumnNames.OWNER,
+      ColumnNames.REPO,
+      ColumnNames.BRANCH,
+    ];
+    await element.updateComplete;
+    const branch = queryAndAssert(element, '.change-header .branch');
+    assert.equal(branch.previousElementSibling?.className, 'cell repo');
+    assert.equal(branch.textContent?.trim(), 'test-branch');
+    assert.equal(
+      queryAndAssert(branch, 'a').getAttribute('href'),
+      '/q/project:a/test/repo+branch:test-branch'
+    );
+    assert.isNotOk(query(element, '.change-metadata .branch'));
+
+    await setViewport({width: 1000, height: 900});
+    await waitUntil(() => !query(element, '.change-header'));
+    assert.isNotOk(query(element, '.change-subject'));
+    assert.isNotOk(query(element, '.number'));
+    assert.isOk(query(element, '.subject'));
+    assert.isOk(query(element, '.branch gr-limited-text'));
+  });
+
   test('renders', async () => {
     await setViewport({width: 1000, height: 900});
     await waitUntil(() => !element.shadowRoot!.querySelector('.change-header'));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 95b2941..8ef63c2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -31,29 +31,16 @@
 import {materialStyles} from '../../../styles/gr-material-styles';
 import {when} from 'lit/directives/when.js';
 import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import {
+  computeLabelShortcut,
+  getChangeListLabels,
+} from '../../../utils/label-util';
+export {computeLabelShortcut} from '../../../utils/label-util';
 
 const NUMBER_FIXED_COLUMNS = 4;
-const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
-const MAX_SHORTCUT_CHARS = 5;
+const MAX_NARROW_LABELS = 5;
 const INVALID_TOKENS = ['limit:', 'age:', '-age:'];
 
-export function computeLabelShortcut(labelName: string) {
-  if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
-    labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
-  }
-  // Compute label shortcut by splitting token by - and capitalizing first
-  // letter of each token.
-  return labelName
-    .split('-')
-    .reduce((previousValue, currentValue) => {
-      if (!currentValue) {
-        return previousValue;
-      }
-      return previousValue + currentValue[0].toUpperCase();
-    }, '')
-    .slice(0, MAX_SHORTCUT_CHARS);
-}
-
 @customElement('gr-change-list-section')
 export class GrChangeListSection extends LitElement {
   @property({type: Array})
@@ -125,9 +112,6 @@
   private isLoggedIn = false;
 
   @state()
-  private narrowLabelCapacity = Infinity;
-
-  @state()
   private narrowScreen = window.matchMedia('(max-width: 50em)').matches;
 
   private labelResizeObserver?: ResizeObserver;
@@ -141,20 +125,10 @@
       const list = (this.getRootNode() as ShadowRoot).host;
       if (!(list instanceof HTMLElement)) return;
       this.labelResizeObserver = new ResizeObserver(() => {
-        // Match the row padding, 24px slots and 2px gaps on narrow screens.
-        const capacity = Math.max(
-          1,
-          Math.floor((list.clientWidth - 24 + 2) / 26)
-        );
         const narrowScreen = window.matchMedia('(max-width: 50em)').matches;
-        if (
-          capacity === this.narrowLabelCapacity &&
-          narrowScreen === this.narrowScreen
-        )
-          return;
+        if (narrowScreen === this.narrowScreen) return;
         cancelAnimationFrame(this.labelResizeFrame);
         this.labelResizeFrame = requestAnimationFrame(() => {
-          this.narrowLabelCapacity = capacity;
           this.narrowScreen = narrowScreen;
         });
       });
@@ -169,8 +143,15 @@
   }
 
   private get narrowLabels() {
-    const labels = [...(this.labelNames ?? [])];
-    if (!this.narrowScreen) return labels;
+    return this.getNarrowLabels();
+  }
+
+  private getNarrowLabels(change?: ChangeInfo) {
+    if (!this.narrowScreen) return [...(this.labelNames ?? [])];
+    const applicableLabels = change ? getChangeListLabels(change) : undefined;
+    const labels = (this.labelNames ?? []).filter(
+      name => !applicableLabels || applicableLabels.includes(name)
+    );
     const priority = ['Code-Review', 'Verified'];
     labels.sort((a, b) => {
       const rank = (name: string) => {
@@ -179,8 +160,7 @@
       };
       return rank(a) - rank(b);
     });
-    const capacity = this.narrowLabelCapacity;
-    return labels.slice(0, labels.length > capacity ? capacity - 1 : capacity);
+    return labels.slice(0, MAX_NARROW_LABELS);
   }
 
   static override get styles() {
@@ -238,27 +218,8 @@
         }
         @media only screen and (max-width: 50em) {
           .groupTitle.narrowVotes {
-            display: flex;
-            justify-content: flex-end;
-            gap: var(--spacing-s);
-            padding: var(--spacing-xs) var(--spacing-m);
-            font-size: var(--font-size-small);
-          }
-          .groupTitle .labelOverflow {
-            display: block;
-          }
-          .groupTitle td:not(.label) {
             display: none;
           }
-          .groupTitle td.label.narrowHidden {
-            display: none;
-          }
-          .groupTitle td.label {
-            flex: 0 0 24px;
-            width: 24px;
-            padding: 0;
-            border: none;
-          }
         }
       `,
     ];
@@ -484,7 +445,7 @@
         .showNumber=${!!this.showNumber}
         .usp=${this.usp}
         .labelNames=${this.labelNames}
-        .narrowLabels=${this.narrowLabels}
+        .narrowLabels=${this.getNarrowLabels(change)}
         .globalIndex=${this.startIndex + index}
         .triggerSelectionCallback=${this.triggerSelectionCallback}
         aria-label=${ariaLabel}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_screenshot_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_screenshot_test.ts
index a101cc8..3d83d8c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_screenshot_test.ts
@@ -13,6 +13,7 @@
 import {GrChangeList} from './gr-change-list';
 import {
   createAccountDetailWithIdNameAndEmail,
+  createApproval,
   createChange,
   createServerInfo,
   createSubmitRequirementResultInfo,
@@ -22,11 +23,13 @@
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
 import {
+  BranchName,
   ChangeInfo,
   NumericChangeId,
   RepoName,
   Timestamp,
 } from '../../../types/common';
+import {SubmitRequirementStatus} from '../../../api/rest-api';
 import {visualDiffDarkTheme, waitUntil} from '../../../test/test-utils';
 
 suite('gr-change-list screenshot tests', () => {
@@ -57,6 +60,62 @@
     await visualDiffDarkTheme(element, 'gr-change-list');
   });
 
+  test('self-contained mobile votes', async () => {
+    await setViewport({width: 390, height: 900});
+    element.changes = createChanges(5).map((change, index) => {
+      const value = [-2, -1, 1, 2, 0][index];
+      return {
+        ...change,
+        project: 'openstack/nova' as RepoName,
+        subject: [
+          'Blocked change',
+          'Needs improvement',
+          'Recommended change',
+          'Approved change',
+          'Waiting for votes',
+        ][index],
+        owner: {...change.owner, name: 'Monty Taylor'},
+        labels: {
+          'Code-Review': {
+            values: {
+              '-2': 'Block',
+              '-1': 'Dislike',
+              '0': 'Neutral',
+              '+1': 'Recommend',
+              '+2': 'Approve',
+            },
+            all: [{...createApproval(), value}],
+          },
+        },
+        submit_requirements: [
+          {
+            ...createSubmitRequirementResultInfo('label:Code-Review=MAX'),
+            name: 'Code-Review',
+            status:
+              value === 2
+                ? SubmitRequirementStatus.SATISFIED
+                : SubmitRequirementStatus.UNSATISFIED,
+          },
+        ],
+      };
+    });
+    await element.updateComplete;
+    await nextFrame();
+    const section = element.shadowRoot!.querySelector<GrChangeListSection>(
+      'gr-change-list-section'
+    )!;
+    await section.updateComplete;
+    const rows = Array.from(
+      section.shadowRoot!.querySelectorAll<GrChangeListItem>(
+        'gr-change-list-item'
+      )
+    );
+    await Promise.all(rows.map(row => row.updateComplete));
+    await nextFrame();
+    await visualDiff(element, 'gr-change-list-mobile-vote-badges');
+    await visualDiffDarkTheme(element, 'gr-change-list-mobile-vote-badges');
+  });
+
   for (const width of [390, 700, 801, 1000, 1400]) {
     test(`responsive list at ${width}px`, async () => {
       await setViewport({width, height: 900});
@@ -76,6 +135,22 @@
           subject:
             'Show label votes and configurable columns on narrow screens',
           owner: {...change.owner, name: 'Monty Taylor Sword Nimi'},
+          ...(width <= 800 && index === 0
+            ? {
+                attention_set: {
+                  [change.owner._account_id!]: {account: change.owner},
+                },
+                branch: 'stable/very-long-branch-name' as BranchName,
+                reviewers: {
+                  REVIEWER: [
+                    {
+                      ...createAccountDetailWithIdNameAndEmail(2),
+                      name: 'Josh',
+                    },
+                  ],
+                },
+              }
+            : {}),
           project: (index === 0
             ? 'openstack/very-long-repository-name'
             : 'gerrit') as RepoName,
@@ -108,24 +183,35 @@
         const first = rows[0].shadowRoot!;
         const number = first.querySelector('.number')!;
         const subject = first.querySelector('.subject')!;
+        assert.isAtLeast(
+          subject.getBoundingClientRect().top,
+          first.querySelector('.change-header')!.getBoundingClientRect().bottom
+        );
         assert.equal(
-          Math.round(number.getBoundingClientRect().top),
-          Math.round(subject.getBoundingClientRect().top)
+          first.querySelector('.change-header')!.getBoundingClientRect().left,
+          subject.getBoundingClientRect().left
         );
         assert.isAtLeast(
-          number.getBoundingClientRect().left,
-          subject.getBoundingClientRect().right
+          first.querySelector('.change-metadata')!.getBoundingClientRect().top,
+          subject.getBoundingClientRect().bottom
         );
-        const votePositions = rows.map(row =>
-          Array.from(row.shadowRoot!.querySelectorAll('.label')).map(cell =>
-            Math.round(cell.getBoundingClientRect().left)
-          )
+        const headerStyle = getComputedStyle(
+          first.querySelector('.change-header')!
         );
-        assert.deepEqual(votePositions[0], votePositions[1]);
-        const headerPositions = Array.from(
-          section.shadowRoot!.querySelectorAll('.groupTitle .label')
-        ).map(cell => Math.round(cell.getBoundingClientRect().left));
-        assert.deepEqual(headerPositions, votePositions[0]);
+        assert.isBelow(
+          parseFloat(headerStyle.fontSize),
+          parseFloat(getComputedStyle(subject).fontSize)
+        );
+        assert.isEmpty(
+          Array.from(
+            section.shadowRoot!.querySelectorAll('.groupTitle .label')
+          ).filter(cell => cell.getBoundingClientRect().width > 0)
+        );
+        assert.isTrue(
+          Array.from(
+            first.querySelectorAll('gr-change-list-column-requirement')
+          ).every(badge => badge.compact)
+        );
         const repo = first.querySelector('.repo')!;
         assert.isAbove(repo.getBoundingClientRect().width, 0);
         const repoLink = repo.querySelector<HTMLElement>('.fullRepo')!;
@@ -134,10 +220,34 @@
         assert.isAtMost(repoLink.scrollWidth, repoLink.clientWidth);
         assert.isAtLeast(
           repo.getBoundingClientRect().left,
-          first.querySelector('.owner')!.getBoundingClientRect().right
+          number.getBoundingClientRect().right
+        );
+        assert.isAtMost(
+          repo.getBoundingClientRect().bottom,
+          subject.getBoundingClientRect().top
+        );
+        const branch = first.querySelector('.change-header .branch')!;
+        assert.equal(branch.previousElementSibling, repo);
+        assert.isNull(first.querySelector('.change-metadata .branch'));
+        const branchLink = branch.querySelector<HTMLElement>('a')!;
+        assert.equal(branchLink.textContent?.trim(), element.changes[0].branch);
+        assert.isAbove(branchLink.clientWidth, 0);
+        assert.isAtMost(branchLink.scrollWidth, branchLink.clientWidth);
+        assert.isAtLeast(
+          branch.getBoundingClientRect().left,
+          repo.getBoundingClientRect().right
+        );
+        assert.isAtMost(
+          branch.getBoundingClientRect().bottom,
+          subject.getBoundingClientRect().top
         );
         const accountLabel = first.querySelector('gr-account-label')!;
         const name = accountLabel.shadowRoot!.querySelector('.name')!;
+        assert.isBelow(
+          parseFloat(getComputedStyle(name).fontSize),
+          parseFloat(getComputedStyle(subject).fontSize)
+        );
+        assert.equal(getComputedStyle(name).fontWeight, '500');
         assert.isAbove(name.clientWidth, 100);
         assert.isAtMost(name.scrollWidth, name.clientWidth);
         assert.isAtMost(element.scrollWidth, element.clientWidth);
@@ -145,6 +255,7 @@
       if (width > 800) {
         for (const row of rows) {
           assert.isNull(row.shadowRoot!.querySelector('.change-header'));
+          assert.isNull(row.shadowRoot!.querySelector('.change-subject'));
           assert.isNull(row.shadowRoot!.querySelector('.change-metadata'));
           assert.isNull(row.shadowRoot!.querySelector('.votes'));
           for (const cell of row.shadowRoot!.querySelectorAll('td')) {
@@ -219,44 +330,35 @@
             '.label:not(.labelOverflow)'
           )
         );
-        assert.lengthOf(votes, 30);
+        assert.lengthOf(votes, width <= 800 && row === rows[1] ? 15 : 30);
         if (width <= 800) {
-          assert.deepEqual(
-            votes
-              .filter(cell => cell.getBoundingClientRect().width > 0)
-              .map(cell => Math.round(cell.getBoundingClientRect().left)),
-            headers
-              .filter(cell => cell.getBoundingClientRect().width > 0)
-              .map(cell => Math.round(cell.getBoundingClientRect().left))
-          );
           assert.isAtMost(row.scrollWidth, row.clientWidth);
-          const voteLines = new Set(
-            votes
-              .filter(cell => cell.getBoundingClientRect().width > 0)
-              .map(cell => Math.round(cell.getBoundingClientRect().top))
-          );
-          const headerLines = new Set(
-            headers
-              .filter(cell => cell.getBoundingClientRect().width > 0)
-              .map(cell => Math.round(cell.getBoundingClientRect().top))
-          );
-          assert.equal(voteLines.size, 1);
-          const visibleHeaders = headers
+          const visibleVotes = votes
             .filter(cell => cell.getBoundingClientRect().width > 0)
             .sort(
               (a, b) =>
                 a.getBoundingClientRect().left - b.getBoundingClientRect().left
             );
-          assert.equal(visibleHeaders[0].title, 'Code-Review');
-          assert.equal(visibleHeaders[1].title, 'Verified');
-          assert.lengthOf(visibleHeaders, width === 390 ? 13 : 25);
+          assert.lengthOf(visibleVotes, 5);
+          assert.equal(
+            visibleVotes[0].querySelector('gr-change-list-column-requirement')!
+              .labelName,
+            'Code-Review'
+          );
+          assert.equal(
+            visibleVotes[1].querySelector('gr-change-list-column-requirement')!
+              .labelName,
+            row === rows[0] ? 'Verified' : 'Check-A-A'
+          );
+          assert.isEmpty(
+            headers.filter(cell => cell.getBoundingClientRect().width > 0)
+          );
           assert.isAbove(
             row
               .shadowRoot!.querySelector('.labelOverflow')!
               .getBoundingClientRect().width,
             0
           );
-          assert.equal(headerLines.size, voteLines.size);
         }
       }
       assert.isAtMost(element.getBoundingClientRect().right, width);
@@ -284,14 +386,14 @@
       }
       if (width === 700) {
         await setViewport({width: 390, height: 900});
-        await waitUntil(
-          () =>
-            Array.from(
-              section.shadowRoot!.querySelectorAll(
-                '.groupTitle .label:not(.labelOverflow)'
-              )
-            ).filter(cell => cell.getBoundingClientRect().width > 0).length ===
-            13
+        await waitUntil(() =>
+          rows.every(
+            row =>
+              Array.from(
+                row.shadowRoot!.querySelectorAll('.label:not(.labelOverflow)')
+              ).filter(cell => cell.getBoundingClientRect().width > 0)
+                .length === 5
+          )
         );
         await section.updateComplete;
         await Promise.all(rows.map(row => row.updateComplete));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index 3419ee0..bb028df 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -36,6 +36,7 @@
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
 import {assert, fixture} from '@open-wc/testing';
+import {setViewport} from '@web/test-runner-commands';
 import {html} from 'lit';
 import {testResolver} from '../../../test/common-test-setup';
 import {Timestamp} from '../../../api/rest-api';
@@ -576,6 +577,7 @@
   });
 
   test('loggedIn and showNumber', async () => {
+    await setViewport({width: 1000, height: 900});
     element.sections = [{results: [{...createChange()}], name: 'a'}];
     element.loggedInUser = {_account_id: 1001 as AccountId};
     element.preferences = {
@@ -620,6 +622,17 @@
     await section.updateComplete;
     assert.isOk(query(query(section, 'gr-change-list-item'), '.star'));
     assert.isOk(query(query(section, 'gr-change-list-item'), '.number'));
+
+    element.showNumber = false;
+    await element.updateComplete;
+    await section.updateComplete;
+    await setViewport({width: 390, height: 900});
+    const row = queryAndAssert<GrChangeListItem>(
+      section,
+      'gr-change-list-item'
+    );
+    await waitUntil(() => !!query(row, '.change-header'));
+    assert.equal(queryAndAssert(row, '.number').textContent?.trim(), '42');
   });
 
   test('garbage columns in preference are not shown', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 91a22f0..d329059 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -678,10 +678,6 @@
             --gr-button-padding: var(--spacing-m);
             white-space: nowrap;
           }
-          gr-button,
-          gr-dropdown {
-            margin: 0;
-          }
           #actionLoadingMessage {
             margin: var(--spacing-m);
             text-align: center;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 5b2bdef..c71f936 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -125,7 +125,7 @@
 import {checksModelToken} from '../../../models/checks/checks-model';
 import {changeModelToken} from '../../../models/change/change-model';
 import {chatModelToken} from '../../../models/chat/chat-model';
-import {css, html, LitElement, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
 import {materialStyles} from '../../../styles/gr-material-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -281,6 +281,9 @@
   private editingCommitMessage = false;
 
   @state()
+  commitMessageMarkdown = false;
+
+  @state()
   private latestCommitMessage = '';
 
   @state() basePatchNum: BasePatchSetNum = PARENT;
@@ -978,6 +981,11 @@
         .commitMessage gr-formatted-text {
           overflow-wrap: anywhere;
         }
+        .commitMessage gr-formatted-text[markdown] {
+          font-family: var(--font-family);
+          font-size: var(--font-size-normal);
+          line-height: var(--line-height-normal);
+        }
         #commitMessageEditor {
           /* Account for border and padding and rounding errors. */
           min-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
@@ -1180,6 +1188,15 @@
         .sidebar {
           height: var(--sidebar-height);
         }
+        .sidebar gr-endpoint-decorator[name='chat-panel'] {
+          display: block;
+          height: 100%;
+        }
+        .sidebar
+          gr-endpoint-decorator[name='chat-panel']
+          > :not(gr-endpoint-param) {
+          height: 100%;
+        }
       `,
     ];
   }
@@ -1262,8 +1279,18 @@
   }
 
   private renderSidebar() {
-    if (!this.showSidebarChat) return;
-    return html`<chat-panel></chat-panel>`;
+    if (!this.showSidebarChat) {
+      return nothing;
+    }
+    return html`
+      <gr-endpoint-decorator name="chat-panel">
+        <gr-endpoint-param name="change" .value=${this.change}>
+        </gr-endpoint-param>
+        <gr-endpoint-param name="revision" .value=${this.revision}>
+        </gr-endpoint-param>
+        <chat-panel></chat-panel>
+      </gr-endpoint-decorator>
+    `;
   }
 
   private renderChangeInfoSection() {
@@ -1443,8 +1470,10 @@
               <gr-editable-content
                 id="commitMessageEditor"
                 .editing=${this.editingCommitMessage}
+                .markdown=${this.commitMessageMarkdown}
                 .content=${this.latestCommitMessage}
                 @editing-changed=${this.handleEditingChanged}
+                @markdown-changed=${this.handleMarkdownChanged}
                 @content-changed=${this.handleContentChanged}
                 .storageKey=${`c${this.change?._number}_rev${this.change?.current_revision}`}
                 .hideEditCommitMessage=${hideEditCommitMessage}
@@ -1453,7 +1482,7 @@
               >
                 <gr-formatted-text
                   .content=${this.latestCommitMessage}
-                  .markdown=${false}
+                  ?markdown=${this.commitMessageMarkdown}
                 ></gr-formatted-text>
               </gr-editable-content>
             </div>
@@ -1781,6 +1810,10 @@
     this.editingCommitMessage = e.detail.value;
   }
 
+  private handleMarkdownChanged(e: ValueChangedEvent<boolean>) {
+    this.commitMessageMarkdown = e.detail.value;
+  }
+
   private handleContentChanged(e: ValueChangedEvent) {
     // optimistic update
     this.latestCommitMessage = e.detail.value ?? '';
@@ -2076,8 +2109,17 @@
     }
   }
 
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('changeNum')) {
+      this.commitMessageMarkdown = false;
+    }
+  }
+
   // Private but used in tests.
   changeChanged(oldChange: ParsedChangeInfo | undefined) {
+    if (oldChange?._number !== this.change?._number) {
+      this.commitMessageMarkdown = false;
+    }
     this.allPatchSets = computeAllPatchSets(this.change);
     if (!this.change) return;
     this.labelsChanged(oldChange?.labels, this.change.labels);
@@ -2436,7 +2478,7 @@
     const classes = ['header'];
     const status = this.computeChangeStatusChips()?.[0];
     if (status) {
-      classes.push(status.toLowerCase());
+      classes.push(status.toLowerCase().replace(/\s/g, '-'));
     }
     if (this.editMode) {
       classes.push('editMode');
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
index 97568af..d47ae47 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
@@ -400,4 +400,60 @@
       document.body.removeChild(container);
     }
   });
+
+  test('commit message markdown preview', async () => {
+    await setViewport({width: 801, height: 900});
+
+    const revisions = createRevisions(3);
+    revisions['3'].commit!.message =
+      '### Test commit\n\nWith **bold** and `code`.';
+
+    const changeModel = testResolver(changeModelToken);
+    changeModel.updateStateChange({
+      ...element.change!,
+      revisions,
+    });
+    await element.updateComplete;
+
+    const container = document.createElement('div');
+    container.style.width = '801px';
+    container.style.height = '900px';
+    container.style.overflow = 'hidden';
+    container.style.display = 'block';
+    container.style.backgroundColor = 'var(--view-background-color, #fff)';
+    container.appendChild(element);
+    document.body.appendChild(container);
+
+    try {
+      await waitUntil(() => !!element.fileList);
+      await element.updateComplete;
+      if (element.fileList) {
+        await element.fileList.updateComplete;
+      }
+      await waitUntil(
+        () => !!element.shadowRoot!.querySelector('gr-file-list')
+      );
+
+      const toggleBtn = element.commitMessageEditor?.shadowRoot?.querySelector(
+        'gr-button.markdown-toggle'
+      ) as HTMLElement | null;
+      toggleBtn?.click();
+      await element.commitMessageEditor?.updateComplete;
+      await element.updateComplete;
+      await waitUntil(
+        () =>
+          !!element.shadowRoot
+            ?.querySelector('gr-formatted-text')
+            ?.shadowRoot?.querySelector('div.markdown-html h3')
+      );
+
+      await visualDiff(container, 'gr-change-view-commit-markdown-801px');
+      await visualDiffDarkTheme(
+        container,
+        'gr-change-view-commit-markdown-801px'
+      );
+    } finally {
+      document.body.removeChild(container);
+    }
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 268fb5e..fd2cdc5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -83,6 +83,7 @@
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {commentsModelToken} from '../../../models/comments/comments-model';
+import {GrEditableContent} from '../../shared/gr-editable-content/gr-editable-content';
 
 suite('gr-change-view tests', () => {
   let element: GrChangeView;
@@ -1651,5 +1652,140 @@
 
     assert.isFalse(contentWithSidebar.hideSide);
     assert.isTrue(processChatRequestStub.calledOnceWithExactly(event.detail));
+    const decorator = queryAndAssert(
+      element,
+      'gr-endpoint-decorator[name="chat-panel"]'
+    );
+    queryAndAssert(decorator, 'chat-panel');
+  });
+
+  test('chat-panel endpoint can be replaced by a plugin', async () => {
+    element.change = {...createChangeViewChange(), labels: {}};
+    element.revision = createRevision();
+    await element.updateComplete;
+
+    const promise = mockPromise();
+    window.Gerrit.install(
+      promise.resolve,
+      '0.1',
+      'http://some/plugins/chat-plugin.js'
+    );
+    const plugin = (await promise) as PluginApi;
+
+    class CustomPluginChatPanel extends HTMLElement {
+      change?: unknown;
+
+      revision?: unknown;
+    }
+
+    const customChatTagName = 'custom-plugin-chat-panel';
+    if (!customElements.get(customChatTagName)) {
+      customElements.define(customChatTagName, CustomPluginChatPanel);
+    }
+
+    plugin.registerCustomComponent('chat-panel', customChatTagName, {
+      replace: true,
+    });
+
+    element.dispatchEvent(
+      new CustomEvent('explain-code-requested', {
+        detail: {prompt: ''},
+        bubbles: true,
+        composed: true,
+      })
+    );
+    await element.updateComplete;
+
+    const decorator = queryAndAssert(
+      element,
+      'gr-endpoint-decorator[name="chat-panel"]'
+    );
+    await waitUntil(() => !!decorator.querySelector(customChatTagName));
+
+    const customChat = queryAndAssert<CustomPluginChatPanel>(
+      decorator,
+      customChatTagName
+    );
+    assert.isNull(decorator.querySelector('chat-panel'));
+    assert.equal(customChat.change, element.change);
+    assert.equal(customChat.revision, element.revision);
+  });
+
+  test('toggles commit message markdown and preserves state across edit', async () => {
+    element.change = createChangeViewChange();
+    element.revision = createRevision();
+    element.loggedIn = true;
+    element.loading = false;
+    await element.updateComplete;
+
+    const editor = queryAndAssert<GrEditableContent>(
+      element,
+      '#commitMessageEditor'
+    );
+    editor.isUploader = true;
+    sinon.stub(editor, 'loadEmails').resolves();
+    await editor.updateComplete;
+
+    const formattedText = queryAndAssert<HTMLElement & {markdown: boolean}>(
+      editor,
+      'gr-formatted-text'
+    );
+    assert.isFalse(formattedText.markdown);
+    assert.isFalse(formattedText.hasAttribute('markdown'));
+
+    const toggleButton = queryAndAssert<HTMLElement>(
+      editor,
+      'gr-button.markdown-toggle'
+    );
+    toggleButton.click();
+    await editor.updateComplete;
+    await element.updateComplete;
+
+    assert.isTrue(formattedText.markdown);
+    assert.isTrue(formattedText.hasAttribute('markdown'));
+
+    // Pressing Edit while in Markdown mode hides both Markdown toggle and Edit buttons
+    const editButton = queryAndAssert<HTMLElement>(
+      editor,
+      'gr-button.edit-commit-message'
+    );
+    editButton.click();
+    await waitUntil(() => editor.editing);
+    await editor.updateComplete;
+    await element.updateComplete;
+
+    assert.isTrue(editor.hideEditCommitMessage);
+    assert.isNull(
+      editor.shadowRoot?.querySelector('gr-button.markdown-toggle')
+    );
+    assert.isNull(
+      editor.shadowRoot?.querySelector('gr-button.edit-commit-message')
+    );
+
+    // Canceling edit restores the viewer with Markdown still enabled
+    const cancelButton = queryAndAssert<HTMLElement>(
+      editor,
+      'gr-button.cancel-button'
+    );
+    cancelButton.click();
+    await editor.updateComplete;
+    await element.updateComplete;
+
+    assert.isFalse(editor.editing);
+    assert.isTrue(element.commitMessageMarkdown);
+    assert.isTrue(formattedText.markdown);
+    assert.isTrue(formattedText.hasAttribute('markdown'));
+
+    // Navigating to a different change resets commitMessageMarkdown to false
+    element.change = {
+      ...createChangeViewChange(),
+      _number: 999 as NumericChangeId,
+    };
+    await element.updateComplete;
+    await editor.updateComplete;
+
+    assert.isFalse(element.commitMessageMarkdown);
+    assert.isFalse(formattedText.markdown);
+    assert.isFalse(formattedText.hasAttribute('markdown'));
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 4e146da..f075d83 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -1565,7 +1565,14 @@
         For example, without a nested div screen readers pronounce the
         "Commit message" row content with incorrect column headers.
         -->
-      <div class=${this.computeClass('', file.__path)}>
+      <div
+        class=${this.computeClass(
+          '',
+          file.__path,
+          /* showForCommitMessage */ false,
+          file.status
+        )}
+      >
         ${when(
           file.diffs_too_expensive_to_compute,
           () => html`
@@ -2523,11 +2530,17 @@
   }
 
   // Private but used in tests.
-  computeClass(baseClass = '', path?: string, showForCommitMessage = false) {
+  computeClass(
+    baseClass = '',
+    path?: string,
+    showForCommitMessage = false,
+    status?: FileInfoStatus
+  ) {
     const classes = [baseClass];
     if (
-      !(showForCommitMessage && path === SpecialFilePath.COMMIT_MESSAGE) &&
-      isMagicPath(path)
+      (!(showForCommitMessage && path === SpecialFilePath.COMMIT_MESSAGE) &&
+        isMagicPath(path)) ||
+      status === FileInfoStatus.UNMODIFIED
     ) {
       classes.push('invisible');
     }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index bcf103b..a39166a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -42,6 +42,7 @@
 import {
   createDefaultDiffPrefs,
   DiffViewMode,
+  FileInfoStatus,
   SpecialFilePath,
 } from '../../../constants/constants';
 import {
@@ -1800,6 +1801,26 @@
           ' May include files outside of this change, also virtual or generated files.'
       );
     });
+
+    test('should hide stats column for unmodified files', async () => {
+      element.files = [
+        ...createFiles(1, {lines_inserted: 5}),
+        ...createFiles(1, {status: FileInfoStatus.UNMODIFIED}),
+      ];
+      await element.updateComplete;
+
+      const fileRows = queryAll<HTMLDivElement>(element, '.file-row');
+      assert.equal(fileRows.length, 2);
+
+      const modifiedStatsContent = queryAndAssert(fileRows[0], '.stats > div');
+      assert.isFalse(modifiedStatsContent.classList.contains('invisible'));
+
+      const unmodifiedStatsContent = queryAndAssert(
+        fileRows[1],
+        '.stats > div'
+      );
+      assert.isTrue(unmodifiedStatsContent.classList.contains('invisible'));
+    });
   });
 
   suite('diff url file list', () => {
@@ -2394,6 +2415,15 @@
         'invisible'
       );
       assert.equal(element.computeClass('', 'file.java'), '');
+      assert.equal(
+        element.computeClass(
+          '',
+          'file.java',
+          /* showForCommitMessage */ false,
+          FileInfoStatus.UNMODIFIED
+        ),
+        'invisible'
+      );
     });
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 50de8a7..5d4b0b6 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -195,6 +195,8 @@
 
   private renderPermittedLabels() {
     const items = this.computePermittedLabelValues();
+    // Material's 48px touch targets overlap adjacent label rows. Restrict
+    // the hit area to the visible button so clicks cannot vote on another label.
     return items.map(
       (value, index) => html`
         <gr-button
@@ -208,6 +210,7 @@
           data-name=${ifDefined(this.label?.name)}
           data-value=${value}
           aria-label=${value}
+          touch-target="none"
           voteChip
           flatten
         >
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
index 77961a8..b4f73a9 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
@@ -296,6 +296,7 @@
               tabindex="0"
               title="bad"
               data-vote="min"
+              touch-target="none"
               votechip=""
               flatten=""
             >
@@ -313,6 +314,7 @@
               tabindex="0"
               data-vote="neutral"
               title="Reset Vote"
+              touch-target="none"
               votechip=""
               flatten=""
             >
@@ -336,6 +338,7 @@
               tabindex="0"
               title="good"
               data-vote="max"
+              touch-target="none"
               votechip=""
               flatten=""
             >
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
index 0def9d8..4210428 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
@@ -21,8 +21,16 @@
 import {ChangeStatus} from '../../../constants/constants';
 import {getVoteForAccount} from '../../../utils/label-util';
 import {assert, fixture, html} from '@open-wc/testing';
+import {
+  executeServerCommand,
+  sendMouse,
+  setViewport,
+} from '@web/test-runner-commands';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {MdTextButton} from '@material/web/button/text-button';
 
 suite('gr-label-scores tests', () => {
+  const input = navigator.maxTouchPoints > 0 ? 'touch' : 'mouse';
   const accountId = 123 as AccountId;
   let element: GrLabelScores;
 
@@ -97,6 +105,80 @@
     );
   });
 
+  for (const width of [1200, 700, 390]) {
+    test(`${input} near a vote button cannot change another label at ${width}px`, async () => {
+      await setViewport({width, height: 800});
+      assert.equal(window.innerWidth, width);
+      let pointerType = '';
+      element.addEventListener('pointerdown', e => {
+        pointerType = e.pointerType;
+      });
+      const rows = ['Code-Review', 'Verified'].map(name =>
+        queryAndAssert<GrLabelScoreRow>(
+          element,
+          `gr-label-score-row[name="${name}"]`
+        )
+      );
+      await Promise.all(rows.map(row => row.updateComplete));
+      for (const row of rows) row.setSelectedValue('-1');
+      await Promise.all(rows.map(row => row.updateComplete));
+      const buttons = rows.map(row =>
+        queryAndAssert<GrButton>(row, 'gr-button[data-value=" 0"]')
+      );
+      await Promise.all(buttons.map(button => button.updateComplete));
+      await Promise.all(
+        buttons.map(
+          button =>
+            queryAndAssert<MdTextButton>(button, 'md-text-button')
+              .updateComplete
+        )
+      );
+      const rects = buttons.map(button => button.getBoundingClientRect());
+      const click = async (rect: DOMRect, y: number) => {
+        const position: [number, number] = [
+          Math.round(rect.x + rect.width / 2),
+          Math.round(y),
+        ];
+        if (input === 'touch') {
+          await executeServerCommand('send-touch-tap', {position});
+        } else {
+          await sendMouse({type: 'click', position});
+        }
+        await Promise.all(rows.map(row => row.updateComplete));
+      };
+
+      // Round away from fractional edges so the points are fully outside
+      // the buttons even with different fonts or device pixel rounding.
+      // Tap adjustment may select the nearby button, but never the other label.
+      await click(rects[0], Math.ceil(rects[0].bottom) + 1);
+      assert.equal(element.getLabelValues().Verified, -1);
+      if (input === 'mouse') {
+        assert.equal(element.getLabelValues()['Code-Review'], -1);
+      }
+      for (const row of rows) row.setSelectedValue('-1');
+      await Promise.all(rows.map(row => row.updateComplete));
+      await click(rects[1], Math.floor(rects[1].top) - 1);
+      assert.equal(element.getLabelValues()['Code-Review'], -1);
+      if (input === 'mouse') {
+        assert.equal(element.getLabelValues().Verified, -1);
+      }
+      for (const row of rows) row.setSelectedValue('-1');
+      await Promise.all(rows.map(row => row.updateComplete));
+
+      await click(rects[0], rects[0].y + rects[0].height / 2);
+      assert.equal(pointerType, input);
+      assert.deepEqual(element.getLabelValues(), {
+        'Code-Review': 0,
+        Verified: -1,
+      });
+      await click(rects[1], rects[1].y + rects[1].height / 2);
+      assert.deepEqual(element.getLabelValues(), {
+        'Code-Review': 0,
+        Verified: 0,
+      });
+    });
+  }
+
   test('get and set label scores', async () => {
     for (const label of Object.keys(element.permittedLabels!)) {
       const row = queryAndAssert<GrLabelScoreRow>(
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index 94beb5b..e0c7501 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -189,11 +189,14 @@
           max-width: var(--account-max-length, 180px);
         }
         .hasAttention .name {
-          font-weight: var(--font-weight-bold);
+          font-weight: var(
+            --account-label-attention-font-weight,
+            var(--font-weight-bold)
+          );
         }
         a.ownerLink {
           text-decoration: none;
-          color: var(--primary-text-color);
+          color: var(--account-label-text-color, var(--primary-text-color));
           display: flex;
           align-items: center;
           gap: 3px;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index eaf5c8c..205fe61 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -11,6 +11,7 @@
 import {addShortcut, getEventPath, Key} from '../../../utils/dom-util';
 import {getAppContext} from '../../../services/app-context';
 import {classMap} from 'lit/directives/class-map.js';
+import {ifDefined} from 'lit/directives/if-defined.js';
 import {Interaction} from '../../../constants/reporting';
 import '@material/web/button/elevated-button';
 import '@material/web/button/text-button';
@@ -35,6 +36,10 @@
   @property({type: Boolean, reflect: true})
   voteChip = false;
 
+  /** Override Material's expanded hit area; undefined keeps its default. */
+  @property({type: String, attribute: 'touch-target'})
+  touchTarget?: 'none' | 'wrapper';
+
   // Note: don't assign a value to this, since constructor is called
   // after created, the initial value maybe overridden by this
   private initialTabindex?: string;
@@ -210,6 +215,7 @@
           class=${buttonClass}
           ?disabled=${this.disabled || this.loading}
           part="md-elevated-button"
+          touch-target=${ifDefined(this.touchTarget)}
           role="button"
           tabindex="-1"
         >
@@ -227,6 +233,7 @@
         class=${buttonClass}
         ?disabled=${this.disabled || this.loading}
         part="md-text-button"
+        touch-target=${ifDefined(this.touchTarget)}
         role="button"
         tabindex="-1"
       >
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 6a6118c..58a7faf 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -63,6 +63,7 @@
   interface HTMLElementEventMap {
     'content-changed': ValueChangedEvent<string>;
     'editing-changed': ValueChangedEvent<boolean>;
+    'markdown-changed': ValueChangedEvent<boolean>;
     /** Fired when the 'cancel' button is pressed. */
     'editable-content-cancel': CustomEvent<{}>;
     /** Fired when the 'save' button is pressed. */
@@ -93,6 +94,9 @@
   })
   editing = false;
 
+  @property({type: Boolean})
+  markdown = false;
+
   @property({type: Boolean, attribute: 'remove-zero-width-space'})
   removeZeroWidthSpace?: boolean;
 
@@ -106,6 +110,9 @@
   @property({type: Boolean, attribute: 'hide-edit-commit-message'})
   hideEditCommitMessage?: boolean;
 
+  @property({type: Boolean, attribute: 'hide-markdown-toggle'})
+  hideMarkdownToggle?: boolean;
+
   /** If false, then the "Show more" button was used to expand. */
   @state() commitCollapsed = true;
 
@@ -353,7 +360,12 @@
   }
 
   private renderButtons() {
-    if (!this.editing && !this.commitCollapsible && this.hideEditCommitMessage)
+    if (
+      !this.editing &&
+      !this.commitCollapsible &&
+      this.hideEditCommitMessage &&
+      this.hideMarkdownToggle
+    )
       return nothing;
 
     return html`
@@ -382,6 +394,26 @@
           `
         )}
         ${when(
+          !this.hideMarkdownToggle && !this.editing,
+          () => html`
+            <gr-button
+              link
+              class="markdown-toggle"
+              title=${this.markdown
+                ? 'Render commit message as plaintext'
+                : 'Render commit message as markdown'}
+              @click=${this.toggleMarkdown}
+              ><div>
+                <gr-icon
+                  icon=${this.markdown ? 'notes' : 'markdown'}
+                  small
+                ></gr-icon>
+                <span>${this.markdown ? 'Plaintext' : 'Markdown'}</span>
+              </div></gr-button
+            >
+          `
+        )}
+        ${when(
           !this.hideEditCommitMessage,
           () => html`
             <gr-button
@@ -632,6 +664,13 @@
     }
   }
 
+  toggleMarkdown() {
+    this.markdown = !this.markdown;
+    fire(this, 'markdown-changed', {
+      value: this.markdown,
+    });
+  }
+
   async handleEditCommitMessage() {
     if (this.editMode) {
       assertIsDefined(this.changeNum, 'changeNum');
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index 1eb6bca..2726d1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -71,6 +71,19 @@
           <div class="flex-space"></div>
           <gr-button
             aria-disabled="false"
+            class="markdown-toggle"
+            link=""
+            role="button"
+            tabindex="0"
+            title="Render commit message as markdown"
+          >
+            <div>
+              <gr-icon icon="markdown" small></gr-icon>
+              <span>Markdown</span>
+            </div>
+          </gr-button>
+          <gr-button
+            aria-disabled="false"
             class="edit-commit-message"
             link=""
             role="button"
@@ -92,9 +105,15 @@
     element.editing = false;
     element.commitCollapsible = false;
     element.hideEditCommitMessage = true;
+    element.hideMarkdownToggle = true;
     await element.updateComplete;
     assert.isNotOk(query(element, '.show-all-container'));
 
+    element.hideMarkdownToggle = false;
+    await element.updateComplete;
+    assert.isOk(query(element, '.show-all-container'));
+
+    element.hideMarkdownToggle = true;
     element.hideEditCommitMessage = false;
     await element.updateComplete;
     assert.isOk(query(element, '.show-all-container'));
@@ -110,6 +129,83 @@
     assert.isOk(query(element, '.show-all-container'));
   });
 
+  test('markdown toggle', async () => {
+    const handler = sinon.spy();
+    element.addEventListener('markdown-changed', handler);
+
+    // Programmatic updates to .markdown should not dispatch markdown-changed
+    element.markdown = true;
+    await element.updateComplete;
+    assert.isFalse(handler.called);
+    element.markdown = false;
+    await element.updateComplete;
+    assert.isFalse(handler.called);
+
+    const toggleButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button.markdown-toggle'
+    );
+    assert.isFalse(element.markdown);
+    assert.equal(toggleButton.textContent?.trim(), 'Markdown');
+
+    toggleButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(element.markdown);
+    assert.isTrue(handler.calledOnce);
+    assert.deepEqual(handler.lastCall.args[0].detail, {value: true});
+    assert.equal(toggleButton.textContent?.trim(), 'Plaintext');
+
+    toggleButton.click();
+    await element.updateComplete;
+
+    assert.isFalse(element.markdown);
+    assert.isTrue(handler.calledTwice);
+    assert.deepEqual(handler.lastCall.args[0].detail, {value: false});
+    assert.equal(toggleButton.textContent?.trim(), 'Markdown');
+  });
+
+  test('editing in markdown mode hides markdown toggle and preserves markdown state', async () => {
+    element.content = '### Heading\n\nBody text';
+    await element.updateComplete;
+
+    const toggleButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button.markdown-toggle'
+    );
+    toggleButton.click();
+    await element.updateComplete;
+    assert.isTrue(element.markdown);
+    assert.isOk(query(element, '.viewer'));
+
+    sinon.stub(element, 'loadEmails').resolves();
+    const editButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button.edit-commit-message'
+    );
+    editButton.click();
+    await waitUntil(() => element.editing);
+    await element.updateComplete;
+
+    assert.isNotOk(query(element, '.viewer'));
+    assert.isOk(query(element, '.editor gr-autogrow-textarea'));
+    assert.isNotOk(query(element, 'gr-button.markdown-toggle'));
+
+    queryAndAssert<GrButton>(element, 'gr-button.cancel-button').click();
+    await element.updateComplete;
+
+    assert.isFalse(element.editing);
+    assert.isTrue(element.markdown);
+    assert.isOk(query(element, '.viewer'));
+    assert.equal(
+      queryAndAssert<GrButton>(
+        element,
+        'gr-button.markdown-toggle'
+      ).textContent?.trim(),
+      'Plaintext'
+    );
+  });
+
   test('save event', async () => {
     element.content = '';
     // Needed because contentChanged resets newContent
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 6c98969..5ef3e38 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../gr-tooltip-content/gr-tooltip-content';
+import '../gr-icon/gr-icon';
 import {css, html, LitElement, TemplateResult} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {
@@ -15,6 +16,7 @@
 import {
   classForLabelStatus,
   getLabelStatus,
+  LabelStatus,
   valueString,
 } from '../../../utils/label-util';
 
@@ -48,6 +50,10 @@
   @property({type: Boolean, attribute: 'tooltip-with-who-voted'})
   tooltipWithWhoVoted = false;
 
+  /** Include the label abbreviation inside a self-contained vote badge. */
+  @property({attribute: 'label-name', reflect: true})
+  labelName?: string;
+
   static override get styles() {
     return [
       css`
@@ -98,6 +104,19 @@
           line-height: var(--gr-vote-chip-width, 16px);
           color: var(--vote-text-color);
         }
+        :host([label-name]) .vote-chip {
+          width: auto;
+          align-items: center;
+          min-width: 16px;
+          padding: 2px 6px;
+          border-radius: 12px;
+          white-space: nowrap;
+        }
+        .vote-chip gr-icon {
+          width: 14px;
+          height: 16px;
+          font-size: 16px;
+        }
         .more > .vote-chip {
           position: relative;
           z-index: 2;
@@ -130,7 +149,9 @@
       title=${this.computeTooltip(renderValue)}
       has-tooltip
     >
-      <div class="vote-chip ${this.computeClass()}">${renderValue}</div>
+      <div class="vote-chip ${this.computeClass()}">
+        ${this.labelName}${renderValue}
+      </div>
       ${this.more
         ? html`<div class="chip-angle ${this.computeClass()}">
             ${renderValue}
@@ -147,13 +168,20 @@
       return '';
     } else if (isDetailedLabelInfo(this.label)) {
       if (this.vote?.value) {
+        if (this.labelName) {
+          const status = getLabelStatus(this.label, this.vote.value);
+          if (status === LabelStatus.APPROVED)
+            return this.renderStatusIcon('check');
+          if (status === LabelStatus.REJECTED)
+            return this.renderStatusIcon('close');
+        }
         return valueString(this.vote.value);
       }
     } else if (isQuickLabelInfo(this.label)) {
       if (this.label.approved) {
-        return html`&#x2713;`; // check mark
+        return this.labelName ? this.renderStatusIcon('check') : html`&#x2713;`; // check mark
       } else if (this.label.rejected) {
-        return html`&#x2717;`; // x mark
+        return this.labelName ? this.renderStatusIcon('close') : html`&#x2717;`; // x mark
       } else if (this.label.disliked || this.label.recommended) {
         return valueString(this.label.value);
       }
@@ -161,6 +189,10 @@
     return '';
   }
 
+  private renderStatusIcon(icon: 'check' | 'close') {
+    return html`<gr-icon icon=${icon}></gr-icon>`;
+  }
+
   private computeClass() {
     if (!this.label) {
       return '';
@@ -177,10 +209,13 @@
     if (!this.label || !isDetailedLabelInfo(this.label)) {
       return '';
     }
-    const voteDescription =
+    const value =
       typeof renderValue === 'string'
-        ? this.label.values?.[renderValue] ?? ''
+        ? renderValue
+        : this.labelName && this.vote
+        ? valueString(this.vote.value)
         : '';
+    const voteDescription = this.label.values?.[value] ?? '';
 
     if (this.tooltipWithWhoVoted && this.vote) {
       return `${this.vote?.name}: ${voteDescription}`;
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
index 52870f3..b7a6e2c 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
@@ -23,6 +23,52 @@
     sinon.stub(getAppContext().flagsService, 'isEnabled').returns(true);
   });
 
+  for (const [value, expected] of [
+    [-2, 'CR'],
+    [-1, 'CR-1'],
+    [1, 'CR+1'],
+    [2, 'CR'],
+  ] as const) {
+    test(`self-contained badge for ${value}`, async () => {
+      const element = await fixture<GrVoteChip>(html`<gr-vote-chip
+        label-name="CR"
+        .label=${{
+          values: {
+            '-2': 'Block',
+            '-1': 'Dislike',
+            '0': 'Neutral',
+            '+1': 'Recommend',
+            '+2': 'Approve',
+          },
+        }}
+        .vote=${{...createApproval(), value}}
+        tooltip-with-who-voted
+      ></gr-vote-chip>`);
+      assert.equal(
+        element.shadowRoot!.querySelector('.vote-chip')!.textContent?.trim(),
+        expected
+      );
+      const icon = element.shadowRoot!.querySelector('gr-icon');
+      if (Math.abs(value) === 2) {
+        assert.equal(icon?.icon, value > 0 ? 'check' : 'close');
+        assert.include(
+          element
+            .shadowRoot!.querySelector('gr-tooltip-content')!
+            .getAttribute('title')!,
+          value > 0 ? 'Approve' : 'Block'
+        );
+      } else {
+        assert.isNull(icon);
+      }
+      element.labelName = undefined;
+      await element.updateComplete;
+      assert.equal(
+        element.shadowRoot!.querySelector('.vote-chip')!.textContent?.trim(),
+        value > 0 ? `+${value}` : `${value}`
+      );
+    });
+  }
+
   suite('with QuickLabelInfo', () => {
     test('renders positive', async () => {
       const labelInfo = {
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index d4a703d..d885087 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {safeStyleSheet, setStyleTextContent} from '../../utils/inner-html-util';
+import {getBaseUrl} from '../../utils/url-util';
 
 const appThemeCss = safeStyleSheet`
   html {
@@ -423,14 +424,14 @@
     /* header and footer */
     --footer-background-color: transparent;
     --footer-border-top: none;
-    --header-background-color: var(--background-color-tertiary);
+    --header-background-color: var(--blue-tonal);
     --header-border-bottom: 1px solid var(--border-color);
     --header-border-image: '';
     --header-box-shadow: none;
     --header-padding: 0 var(--spacing-l);
-    --header-icon-size: 0em;
+    --header-icon-size: 1em;
     --header-icon: none;
-    --header-text-color: black;
+    --header-text-color: var(--gray-50);
     --header-title-content: 'Gerrit';
     --header-title-font-size: 1.75rem;
     /* This is just an initial value. Will be updated by a ResizeObserver. */
@@ -580,7 +581,19 @@
   }
 `;
 
-const styleEl = document.createElement('style');
-styleEl.setAttribute('id', 'light-theme');
-setStyleTextContent(styleEl, appThemeCss);
-document.head.appendChild(styleEl);
+export function applyTheme() {
+  if (document.head.querySelector('#light-theme')) return;
+  const styleEl = document.createElement('style');
+  styleEl.setAttribute('id', 'light-theme');
+  setStyleTextContent(styleEl, appThemeCss);
+  document.head.appendChild(styleEl);
+
+  // Keep the default in the stylesheet so custom themes can override it.
+  const rootRule = styleEl.sheet!.cssRules[0] as CSSStyleRule;
+  rootRule.style.setProperty(
+    '--header-icon',
+    `url("${getBaseUrl()}/diffy.png")`
+  );
+}
+
+applyTheme();
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 8c6bb7a..b9f01ee 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -205,10 +205,10 @@
     /* header and footer */
     --footer-background-color: var(--background-color-tertiary);
     --footer-border-top: 1px solid var(--border-color);
-    --header-background-color: var(--background-color-tertiary);
+    --header-background-color: var(--blue-tonal);
     --header-border-bottom: 1px solid var(--border-color);
     --header-padding: 0 var(--spacing-l);
-    --header-text-color: var(--primary-text-color);
+    --header-text-color: var(--gray-50);
 
     /* dashboard size background colors */
     --dashboard-size-xs: var(--gray-700);
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 7347402..4c9a7a5 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -507,6 +507,14 @@
     });
 }
 
+/** Names of applicable requirements and trigger votes shown in a change list. */
+export function getChangeListLabels(change?: ParsedChangeInfo | ChangeInfo) {
+  return [
+    ...getRequirements(change).map(requirement => requirement.name),
+    ...getTriggerVotes(change),
+  ].filter(unique);
+}
+
 export function getTriggerVotes(change?: ParsedChangeInfo | ChangeInfo) {
   const allLabels = Object.keys(change?.labels ?? {});
   // Normally there is utility method getRequirements, which filter out
@@ -551,3 +559,23 @@
     atom => atom.match(/^label[0-9]*:[\w-]+=MIN$/)
   );
 }
+
+const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+const MAX_SHORTCUT_CHARS = 5;
+
+export function computeLabelShortcut(labelName: string) {
+  if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
+    labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
+  }
+  // Compute label shortcut by splitting token by - and capitalizing first
+  // letter of each token.
+  return labelName
+    .split('-')
+    .reduce((previousValue, currentValue) => {
+      if (!currentValue) {
+        return previousValue;
+      }
+      return previousValue + currentValue[0].toUpperCase();
+    }, '')
+    .slice(0, MAX_SHORTCUT_CHARS);
+}
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px-dark.png
index 9ea4a2f..83a6b0e 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px.png
index 0a52c1d..1cd7c89a 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-390px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px-dark.png
index 96a564b..c3ae9bb 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px.png
index 4d1649d..b19d612 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-30-labels-700px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px-dark.png
index efbb560..84c8a71 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px.png
index 5466387..cadf1f0 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-390px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px-dark.png
index add3b3a..0e2201f 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px.png
index 3d48684..069cdb6 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-700px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-dark.png
index 9b32d66..4583220 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges-dark.png
new file mode 100644
index 0000000..2d0d747
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges.png
new file mode 100644
index 0000000..9329f52
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list-mobile-vote-badges.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png
index 9689a45..f21f51c 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open-dark.png
index ac50c8e..43a663d 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open.png
index dd975a0..0faafba 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-1280px-chat-open.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px-dark.png
index 5bf9965..e509546 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px.png
index b7b7977..a3a06eb 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-801px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px-dark.png
new file mode 100644
index 0000000..aacf454
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px.png
new file mode 100644
index 0000000..8b9304b
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-commit-markdown-801px.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px-dark.png
index bc9fb48..e0a04a5 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px.png
index d5b01a9..301ef63 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-view-wrapped-statuses-801px.png
Binary files differ
diff --git a/polygerrit-ui/web-test-runner.config.mjs b/polygerrit-ui/web-test-runner.config.mjs
index c8f9632..f759108 100644
--- a/polygerrit-ui/web-test-runner.config.mjs
+++ b/polygerrit-ui/web-test-runner.config.mjs
@@ -18,6 +18,7 @@
     <!DOCTYPE html>
     <html>
       <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1">
         <link rel="stylesheet" href="${prefix}app/styles/main.css">
         <link rel="stylesheet" href="${prefix}app/styles/fonts.css">
         <link rel="stylesheet" href="${prefix}app/styles/material-icons.css">
@@ -61,6 +62,7 @@
 const pathPrefix = runUnderBazel ? 'polygerrit-ui/' : '';
 const testFiles = getArgValue('--test-files');
 const runScreenshots = process.argv.includes('--run-screenshots');
+const touchTestFile = `${pathPrefix}app/elements/change/gr-label-scores/gr-label-scores_test.ts`;
 // Under Bazel, module imports may resolve through runfile symlinks to
 // rules_js package stores. Both the runfiles tree and those stores are under
 // bazel-out/<config>/bin, so use that as the WTR root.
@@ -84,33 +86,47 @@
   '/usr/bin/google-chrome',
 ].find(p => p && fs.existsSync(p));
 
+// WORKAROUND: Prevents tests from failing or timing out when run concurrently.
+// Recent Chrome versions aggressively throttle inactive tabs, which interferes with
+// parallel tests. These flags disable that behavior, ensuring tests that rely on
+// visibility or timers run reliably.
+// Some details: https://g-issues.gerritcodereview.com/issues/365565157
+function chromeLauncher(touch = false) {
+  return playwrightLauncher({
+    product: 'chromium',
+    ...(touch ? {
+      createBrowserContext: ({browser}) => browser.newContext({
+        hasTouch: true,
+        isMobile: true,
+      }),
+    } : {}),
+    launchOptions: {
+      ...(chromeExecutablePath ? { executablePath: chromeExecutablePath } : {}),
+      args: [
+        '--no-sandbox',
+        '--disable-dev-shm-usage',
+        '--disable-background-timer-throttling',
+        '--disable-backgrounding-occluded-windows',
+        '--disable-renderer-backgrounding',
+        '--font-render-hinting=none',
+        '--disable-font-subpixel-rendering',
+        '--disable-lcd-text',
+      ],
+    },
+  });
+}
+
 /** @type {import('@web/test-runner').TestRunnerConfig} */
 const config = {
   // Default is CPU cores / 2. Use default
   ...(runUnderBazel ? { concurrency: 1 } : {}),
-  // WORKAROUND: Prevents tests from failing or timing out when run concurrently.
-  // Recent Chrome versions aggressively throttle inactive tabs, which interferes with
-  // parallel tests. These flags disable that behavior, ensuring tests that rely on
-  // visibility or timers run reliably.
-  // Some details: https://g-issues.gerritcodereview.com/issues/365565157
-  browsers: [
-    playwrightLauncher({
-      product: 'chromium',
-      launchOptions: {
-        ...(chromeExecutablePath ? { executablePath: chromeExecutablePath } : {}),
-        args: [
-          '--no-sandbox',
-          '--disable-dev-shm-usage',
-          '--disable-background-timer-throttling',
-          '--disable-backgrounding-occluded-windows',
-          '--disable-renderer-backgrounding',
-          '--font-render-hinting=none',
-          '--disable-font-subpixel-rendering',
-          '--disable-lcd-text',
-        ],
-      },
-    }),
-  ],
+  browsers: [chromeLauncher()],
+  // Run vote hit-area regressions with native touch input as well as a mouse.
+  groups: runScreenshots || !fs.existsSync(touchTestFile) ? [] : [{
+    name: 'touch',
+    files: [touchTestFile],
+    browsers: [chromeLauncher(true)],
+  }],
 
   files: runScreenshots
     ? [
@@ -153,6 +169,18 @@
   },
 
   plugins: [
+    {
+      name: 'touch-commands',
+      async executeCommand({command, payload, session}) {
+        if (command !== 'send-touch-tap') return;
+        if (session.browser.type !== 'playwright') {
+          throw new Error('Touch taps require the Playwright launcher');
+        }
+        const page = session.browser.getPage(session.id);
+        await page.touchscreen.tap(...payload.position);
+        return true;
+      },
+    },
     esbuildPlugin({
       ts: true,
       target: 'es2020',
diff --git a/resources/com/google/gerrit/acceptance/BUILD b/resources/com/google/gerrit/acceptance/BUILD
deleted file mode 100644
index 38da575..0000000
--- a/resources/com/google/gerrit/acceptance/BUILD
+++ /dev/null
@@ -1,8 +0,0 @@
-filegroup(
-    name = "acceptance",
-    srcs = glob(
-        ["**/*"],
-        exclude = ["BUILD"],
-    ),
-    visibility = ["//visibility:public"],
-)
diff --git a/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt b/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt
deleted file mode 100644
index 4231f76..0000000
--- a/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt
+++ /dev/null
@@ -1,1532 +0,0 @@
-# Version 2017032102, Last Updated Wed Mar 22 07:07:01 2017 UTC
-# From http://data.iana.org/TLD/tlds-alpha-by-domain.txt
-AAA
-AARP
-ABARTH
-ABB
-ABBOTT
-ABBVIE
-ABC
-ABLE
-ABOGADO
-ABUDHABI
-AC
-ACADEMY
-ACCENTURE
-ACCOUNTANT
-ACCOUNTANTS
-ACO
-ACTIVE
-ACTOR
-AD
-ADAC
-ADS
-ADULT
-AE
-AEG
-AERO
-AETNA
-AF
-AFAMILYCOMPANY
-AFL
-#! AFRICA
-AG
-AGAKHAN
-AGENCY
-AI
-AIG
-AIGO
-AIRBUS
-AIRFORCE
-AIRTEL
-AKDN
-AL
-ALFAROMEO
-ALIBABA
-ALIPAY
-ALLFINANZ
-ALLSTATE
-ALLY
-ALSACE
-ALSTOM
-AM
-AMERICANEXPRESS
-AMERICANFAMILY
-AMEX
-AMFAM
-AMICA
-AMSTERDAM
-ANALYTICS
-ANDROID
-ANQUAN
-ANZ
-AO
-AOL
-APARTMENTS
-APP
-APPLE
-AQ
-AQUARELLE
-AR
-ARAMCO
-ARCHI
-ARMY
-ARPA
-ART
-ARTE
-AS
-ASDA
-ASIA
-ASSOCIATES
-AT
-ATHLETA
-ATTORNEY
-AU
-AUCTION
-AUDI
-AUDIBLE
-AUDIO
-AUSPOST
-AUTHOR
-AUTO
-AUTOS
-AVIANCA
-AW
-AWS
-AX
-AXA
-AZ
-AZURE
-BA
-BABY
-BAIDU
-BANAMEX
-BANANAREPUBLIC
-BAND
-BANK
-BAR
-BARCELONA
-BARCLAYCARD
-BARCLAYS
-BAREFOOT
-BARGAINS
-BASEBALL
-BASKETBALL
-BAUHAUS
-BAYERN
-BB
-BBC
-BBT
-BBVA
-BCG
-BCN
-BD
-BE
-BEATS
-BEAUTY
-BEER
-BENTLEY
-BERLIN
-BEST
-BESTBUY
-BET
-BF
-BG
-BH
-BHARTI
-BI
-BIBLE
-BID
-BIKE
-BING
-BINGO
-BIO
-BIZ
-BJ
-BLACK
-BLACKFRIDAY
-BLANCO
-BLOCKBUSTER
-BLOG
-BLOOMBERG
-BLUE
-BM
-BMS
-BMW
-BN
-BNL
-BNPPARIBAS
-BO
-BOATS
-BOEHRINGER
-BOFA
-BOM
-BOND
-BOO
-BOOK
-BOOKING
-BOOTS
-BOSCH
-BOSTIK
-BOSTON
-BOT
-BOUTIQUE
-BOX
-BR
-BRADESCO
-BRIDGESTONE
-BROADWAY
-BROKER
-BROTHER
-BRUSSELS
-BS
-BT
-BUDAPEST
-BUGATTI
-BUILD
-BUILDERS
-BUSINESS
-BUY
-BUZZ
-BV
-BW
-BY
-BZ
-BZH
-CA
-CAB
-CAFE
-CAL
-CALL
-CALVINKLEIN
-CAM
-CAMERA
-CAMP
-CANCERRESEARCH
-CANON
-CAPETOWN
-CAPITAL
-CAPITALONE
-CAR
-CARAVAN
-CARDS
-CARE
-CAREER
-CAREERS
-CARS
-CARTIER
-CASA
-CASE
-CASEIH
-CASH
-CASINO
-CAT
-CATERING
-CATHOLIC
-CBA
-CBN
-CBRE
-CBS
-CC
-CD
-CEB
-CENTER
-CEO
-CERN
-CF
-CFA
-CFD
-CG
-CH
-CHANEL
-CHANNEL
-CHASE
-CHAT
-CHEAP
-CHINTAI
-CHLOE
-CHRISTMAS
-CHROME
-CHRYSLER
-CHURCH
-CI
-CIPRIANI
-CIRCLE
-CISCO
-CITADEL
-CITI
-CITIC
-CITY
-CITYEATS
-CK
-CL
-CLAIMS
-CLEANING
-CLICK
-CLINIC
-CLINIQUE
-CLOTHING
-CLOUD
-CLUB
-CLUBMED
-CM
-CN
-CO
-COACH
-CODES
-COFFEE
-COLLEGE
-COLOGNE
-COM
-COMCAST
-COMMBANK
-COMMUNITY
-COMPANY
-COMPARE
-COMPUTER
-COMSEC
-CONDOS
-CONSTRUCTION
-CONSULTING
-CONTACT
-CONTRACTORS
-COOKING
-COOKINGCHANNEL
-COOL
-COOP
-CORSICA
-COUNTRY
-COUPON
-COUPONS
-COURSES
-CR
-CREDIT
-CREDITCARD
-CREDITUNION
-CRICKET
-CROWN
-CRS
-CRUISE
-CRUISES
-CSC
-CU
-CUISINELLA
-CV
-CW
-CX
-CY
-CYMRU
-CYOU
-CZ
-DABUR
-DAD
-DANCE
-DATA
-DATE
-DATING
-DATSUN
-DAY
-DCLK
-DDS
-DE
-DEAL
-DEALER
-DEALS
-DEGREE
-DELIVERY
-DELL
-DELOITTE
-DELTA
-DEMOCRAT
-DENTAL
-DENTIST
-DESI
-DESIGN
-DEV
-DHL
-DIAMONDS
-DIET
-DIGITAL
-DIRECT
-DIRECTORY
-DISCOUNT
-DISCOVER
-DISH
-DIY
-DJ
-DK
-DM
-DNP
-DO
-DOCS
-DOCTOR
-DODGE
-DOG
-DOHA
-DOMAINS
-DOT
-DOWNLOAD
-DRIVE
-DTV
-DUBAI
-DUCK
-DUNLOP
-DUNS
-DUPONT
-DURBAN
-DVAG
-DVR
-DZ
-EARTH
-EAT
-EC
-ECO
-EDEKA
-EDU
-EDUCATION
-EE
-EG
-EMAIL
-EMERCK
-ENERGY
-ENGINEER
-ENGINEERING
-ENTERPRISES
-EPOST
-EPSON
-EQUIPMENT
-ER
-ERICSSON
-ERNI
-ES
-ESQ
-ESTATE
-ESURANCE
-ET
-EU
-EUROVISION
-EUS
-EVENTS
-EVERBANK
-EXCHANGE
-EXPERT
-EXPOSED
-EXPRESS
-EXTRASPACE
-FAGE
-FAIL
-FAIRWINDS
-FAITH
-FAMILY
-FAN
-FANS
-FARM
-FARMERS
-FASHION
-FAST
-FEDEX
-FEEDBACK
-FERRARI
-FERRERO
-FI
-FIAT
-FIDELITY
-FIDO
-FILM
-FINAL
-FINANCE
-FINANCIAL
-FIRE
-FIRESTONE
-FIRMDALE
-FISH
-FISHING
-FIT
-FITNESS
-FJ
-FK
-FLICKR
-FLIGHTS
-FLIR
-FLORIST
-FLOWERS
-FLY
-FM
-FO
-FOO
-FOOD
-FOODNETWORK
-FOOTBALL
-FORD
-FOREX
-FORSALE
-FORUM
-FOUNDATION
-FOX
-FR
-FREE
-FRESENIUS
-FRL
-FROGANS
-FRONTDOOR
-FRONTIER
-FTR
-FUJITSU
-FUJIXEROX
-FUN
-FUND
-FURNITURE
-FUTBOL
-FYI
-GA
-GAL
-GALLERY
-GALLO
-GALLUP
-GAME
-GAMES
-GAP
-GARDEN
-GB
-GBIZ
-GD
-GDN
-GE
-GEA
-GENT
-GENTING
-GEORGE
-GF
-GG
-GGEE
-GH
-GI
-GIFT
-GIFTS
-GIVES
-GIVING
-GL
-GLADE
-GLASS
-GLE
-GLOBAL
-GLOBO
-GM
-GMAIL
-GMBH
-GMO
-GMX
-GN
-GODADDY
-GOLD
-GOLDPOINT
-GOLF
-GOO
-GOODHANDS
-GOODYEAR
-GOOG
-GOOGLE
-GOP
-GOT
-GOV
-GP
-GQ
-GR
-GRAINGER
-GRAPHICS
-GRATIS
-GREEN
-GRIPE
-GROUP
-GS
-GT
-GU
-GUARDIAN
-GUCCI
-GUGE
-GUIDE
-GUITARS
-GURU
-GW
-GY
-HAIR
-HAMBURG
-HANGOUT
-HAUS
-HBO
-HDFC
-HDFCBANK
-HEALTH
-HEALTHCARE
-HELP
-HELSINKI
-HERE
-HERMES
-HGTV
-HIPHOP
-HISAMITSU
-HITACHI
-HIV
-HK
-HKT
-HM
-HN
-HOCKEY
-HOLDINGS
-HOLIDAY
-HOMEDEPOT
-HOMEGOODS
-HOMES
-HOMESENSE
-HONDA
-HONEYWELL
-HORSE
-HOSPITAL
-HOST
-HOSTING
-HOT
-HOTELES
-HOTMAIL
-HOUSE
-HOW
-HR
-HSBC
-HT
-HTC
-HU
-HUGHES
-HYATT
-HYUNDAI
-IBM
-ICBC
-ICE
-ICU
-ID
-IE
-IEEE
-IFM
-IKANO
-IL
-IM
-IMAMAT
-IMDB
-IMMO
-IMMOBILIEN
-IN
-INDUSTRIES
-INFINITI
-INFO
-ING
-INK
-INSTITUTE
-INSURANCE
-INSURE
-INT
-INTEL
-INTERNATIONAL
-INTUIT
-INVESTMENTS
-IO
-IPIRANGA
-IQ
-IR
-IRISH
-IS
-ISELECT
-ISMAILI
-IST
-ISTANBUL
-IT
-ITAU
-ITV
-IVECO
-IWC
-JAGUAR
-JAVA
-JCB
-JCP
-JE
-JEEP
-JETZT
-JEWELRY
-JIO
-JLC
-JLL
-JM
-JMP
-JNJ
-JO
-JOBS
-JOBURG
-JOT
-JOY
-JP
-JPMORGAN
-JPRS
-JUEGOS
-JUNIPER
-KAUFEN
-KDDI
-KE
-KERRYHOTELS
-KERRYLOGISTICS
-KERRYPROPERTIES
-KFH
-KG
-KH
-KI
-KIA
-KIM
-KINDER
-KINDLE
-KITCHEN
-KIWI
-KM
-KN
-KOELN
-KOMATSU
-KOSHER
-KP
-KPMG
-KPN
-KR
-KRD
-KRED
-KUOKGROUP
-KW
-KY
-KYOTO
-KZ
-LA
-LACAIXA
-LADBROKES
-LAMBORGHINI
-LAMER
-LANCASTER
-LANCIA
-LANCOME
-LAND
-LANDROVER
-LANXESS
-LASALLE
-LAT
-LATINO
-LATROBE
-LAW
-LAWYER
-LB
-LC
-LDS
-LEASE
-LECLERC
-LEFRAK
-LEGAL
-LEGO
-LEXUS
-LGBT
-LI
-LIAISON
-LIDL
-LIFE
-LIFEINSURANCE
-LIFESTYLE
-LIGHTING
-LIKE
-LILLY
-LIMITED
-LIMO
-LINCOLN
-LINDE
-LINK
-LIPSY
-LIVE
-LIVING
-LIXIL
-LK
-LOAN
-LOANS
-LOCKER
-LOCUS
-LOFT
-LOL
-LONDON
-LOTTE
-LOTTO
-LOVE
-LPL
-LPLFINANCIAL
-LR
-LS
-LT
-LTD
-LTDA
-LU
-LUNDBECK
-LUPIN
-LUXE
-LUXURY
-LV
-LY
-MA
-MACYS
-MADRID
-MAIF
-MAISON
-MAKEUP
-MAN
-MANAGEMENT
-MANGO
-MARKET
-MARKETING
-MARKETS
-MARRIOTT
-MARSHALLS
-MASERATI
-MATTEL
-MBA
-MC
-MCD
-MCDONALDS
-MCKINSEY
-MD
-ME
-MED
-MEDIA
-MEET
-MELBOURNE
-MEME
-MEMORIAL
-MEN
-MENU
-MEO
-METLIFE
-MG
-MH
-MIAMI
-MICROSOFT
-MIL
-MINI
-MINT
-MIT
-MITSUBISHI
-MK
-ML
-MLB
-MLS
-MM
-MMA
-MN
-MO
-MOBI
-MOBILE
-MOBILY
-MODA
-MOE
-MOI
-MOM
-MONASH
-MONEY
-MONSTER
-MONTBLANC
-MOPAR
-MORMON
-MORTGAGE
-MOSCOW
-MOTO
-MOTORCYCLES
-MOV
-MOVIE
-MOVISTAR
-MP
-MQ
-MR
-MS
-MSD
-MT
-MTN
-MTPC
-MTR
-MU
-MUSEUM
-MUTUAL
-MV
-MW
-MX
-MY
-MZ
-NA
-NAB
-NADEX
-NAGOYA
-NAME
-NATIONWIDE
-NATURA
-NAVY
-NBA
-NC
-NE
-NEC
-NET
-NETBANK
-NETFLIX
-NETWORK
-NEUSTAR
-NEW
-NEWHOLLAND
-NEWS
-NEXT
-NEXTDIRECT
-NEXUS
-NF
-NFL
-NG
-NGO
-NHK
-NI
-NICO
-NIKE
-NIKON
-NINJA
-NISSAN
-NISSAY
-NL
-NO
-NOKIA
-NORTHWESTERNMUTUAL
-NORTON
-NOW
-NOWRUZ
-NOWTV
-NP
-NR
-NRA
-NRW
-NTT
-NU
-NYC
-NZ
-OBI
-OBSERVER
-OFF
-OFFICE
-OKINAWA
-OLAYAN
-OLAYANGROUP
-OLDNAVY
-OLLO
-OM
-OMEGA
-ONE
-ONG
-ONL
-ONLINE
-ONYOURSIDE
-OOO
-OPEN
-ORACLE
-ORANGE
-ORG
-ORGANIC
-ORIENTEXPRESS
-ORIGINS
-OSAKA
-OTSUKA
-OTT
-OVH
-PA
-PAGE
-PAMPEREDCHEF
-PANASONIC
-PANERAI
-PARIS
-PARS
-PARTNERS
-PARTS
-PARTY
-PASSAGENS
-PAY
-PCCW
-PE
-PET
-PF
-PFIZER
-PG
-PH
-PHARMACY
-PHILIPS
-PHONE
-PHOTO
-PHOTOGRAPHY
-PHOTOS
-PHYSIO
-PIAGET
-PICS
-PICTET
-PICTURES
-PID
-PIN
-PING
-PINK
-PIONEER
-PIZZA
-PK
-PL
-PLACE
-PLAY
-PLAYSTATION
-PLUMBING
-PLUS
-PM
-PN
-PNC
-POHL
-POKER
-POLITIE
-PORN
-POST
-PR
-PRAMERICA
-PRAXI
-PRESS
-PRIME
-PRO
-PROD
-PRODUCTIONS
-PROF
-PROGRESSIVE
-PROMO
-PROPERTIES
-PROPERTY
-PROTECTION
-PRU
-PRUDENTIAL
-PS
-PT
-PUB
-PW
-PWC
-PY
-QA
-QPON
-QUEBEC
-QUEST
-QVC
-RACING
-RADIO
-RAID
-RE
-READ
-REALESTATE
-REALTOR
-REALTY
-RECIPES
-RED
-REDSTONE
-REDUMBRELLA
-REHAB
-REISE
-REISEN
-REIT
-RELIANCE
-REN
-RENT
-RENTALS
-REPAIR
-REPORT
-REPUBLICAN
-REST
-RESTAURANT
-REVIEW
-REVIEWS
-REXROTH
-RICH
-RICHARDLI
-RICOH
-RIGHTATHOME
-RIL
-RIO
-RIP
-RMIT
-RO
-ROCHER
-ROCKS
-RODEO
-ROGERS
-ROOM
-RS
-RSVP
-RU
-RUHR
-RUN
-RW
-RWE
-RYUKYU
-SA
-SAARLAND
-SAFE
-SAFETY
-SAKURA
-SALE
-SALON
-SAMSCLUB
-SAMSUNG
-SANDVIK
-SANDVIKCOROMANT
-SANOFI
-SAP
-SAPO
-SARL
-SAS
-SAVE
-SAXO
-SB
-SBI
-SBS
-SC
-SCA
-SCB
-SCHAEFFLER
-SCHMIDT
-SCHOLARSHIPS
-SCHOOL
-SCHULE
-SCHWARZ
-SCIENCE
-SCJOHNSON
-SCOR
-SCOT
-SD
-SE
-SEAT
-SECURE
-SECURITY
-SEEK
-SELECT
-SENER
-SERVICES
-SES
-SEVEN
-SEW
-SEX
-SEXY
-SFR
-SG
-SH
-SHANGRILA
-SHARP
-SHAW
-SHELL
-SHIA
-SHIKSHA
-SHOES
-SHOP
-SHOPPING
-SHOUJI
-SHOW
-SHOWTIME
-SHRIRAM
-SI
-SILK
-SINA
-SINGLES
-SITE
-SJ
-SK
-SKI
-SKIN
-SKY
-SKYPE
-SL
-SLING
-SM
-SMART
-SMILE
-SN
-SNCF
-SO
-SOCCER
-SOCIAL
-SOFTBANK
-SOFTWARE
-SOHU
-SOLAR
-SOLUTIONS
-SONG
-SONY
-SOY
-SPACE
-SPIEGEL
-SPOT
-SPREADBETTING
-SR
-SRL
-SRT
-ST
-STADA
-STAPLES
-STAR
-STARHUB
-STATEBANK
-STATEFARM
-STATOIL
-STC
-STCGROUP
-STOCKHOLM
-STORAGE
-STORE
-STREAM
-STUDIO
-STUDY
-STYLE
-SU
-SUCKS
-SUPPLIES
-SUPPLY
-SUPPORT
-SURF
-SURGERY
-SUZUKI
-SV
-SWATCH
-SWIFTCOVER
-SWISS
-SX
-SY
-SYDNEY
-SYMANTEC
-SYSTEMS
-SZ
-TAB
-TAIPEI
-TALK
-TAOBAO
-TARGET
-TATAMOTORS
-TATAR
-TATTOO
-TAX
-TAXI
-TC
-TCI
-TD
-TDK
-TEAM
-TECH
-TECHNOLOGY
-TEL
-TELECITY
-TELEFONICA
-TEMASEK
-TENNIS
-TEVA
-TF
-TG
-TH
-THD
-THEATER
-THEATRE
-TIAA
-TICKETS
-TIENDA
-TIFFANY
-TIPS
-TIRES
-TIROL
-TJ
-TJMAXX
-TJX
-TK
-TKMAXX
-TL
-TM
-TMALL
-TN
-TO
-TODAY
-TOKYO
-TOOLS
-TOP
-TORAY
-TOSHIBA
-TOTAL
-TOURS
-TOWN
-TOYOTA
-TOYS
-TR
-TRADE
-TRADING
-TRAINING
-TRAVEL
-TRAVELCHANNEL
-TRAVELERS
-TRAVELERSINSURANCE
-TRUST
-TRV
-TT
-TUBE
-TUI
-TUNES
-TUSHU
-TV
-TVS
-TW
-TZ
-UA
-UBANK
-UBS
-UCONNECT
-UG
-UK
-UNICOM
-UNIVERSITY
-UNO
-UOL
-UPS
-US
-UY
-UZ
-VA
-VACATIONS
-VANA
-VANGUARD
-VC
-VE
-VEGAS
-VENTURES
-VERISIGN
-VERSICHERUNG
-VET
-VG
-VI
-VIAJES
-VIDEO
-VIG
-VIKING
-VILLAS
-VIN
-VIP
-VIRGIN
-VISA
-VISION
-VISTA
-VISTAPRINT
-VIVA
-VIVO
-VLAANDEREN
-VN
-VODKA
-VOLKSWAGEN
-VOLVO
-VOTE
-VOTING
-VOTO
-VOYAGE
-VU
-VUELOS
-WALES
-WALMART
-WALTER
-WANG
-WANGGOU
-WARMAN
-WATCH
-WATCHES
-WEATHER
-WEATHERCHANNEL
-WEBCAM
-WEBER
-WEBSITE
-WED
-WEDDING
-WEIBO
-WEIR
-WF
-WHOSWHO
-WIEN
-WIKI
-WILLIAMHILL
-WIN
-WINDOWS
-WINE
-WINNERS
-WME
-WOLTERSKLUWER
-WOODSIDE
-WORK
-WORKS
-WORLD
-WOW
-WS
-WTC
-WTF
-XBOX
-XEROX
-XFINITY
-XIHUAN
-XIN
-XN--11B4C3D
-XN--1CK2E1B
-XN--1QQW23A
-XN--30RR7Y
-XN--3BST00M
-XN--3DS443G
-XN--3E0B707E
-XN--3OQ18VL8PN36A
-XN--3PXU8K
-XN--42C2D9A
-XN--45BRJ9C
-XN--45Q11C
-XN--4GBRIM
-XN--54B7FTA0CC
-XN--55QW42G
-XN--55QX5D
-XN--5SU34J936BGSG
-XN--5TZM5G
-XN--6FRZ82G
-XN--6QQ986B3XL
-XN--80ADXHKS
-XN--80AO21A
-XN--80AQECDR1A
-XN--80ASEHDB
-XN--80ASWG
-XN--8Y0A063A
-XN--90A3AC
-XN--90AE
-XN--90AIS
-XN--9DBQ2A
-XN--9ET52U
-XN--9KRT00A
-XN--B4W605FERD
-XN--BCK1B9A5DRE4C
-XN--C1AVG
-XN--C2BR7G
-XN--CCK2B3B
-XN--CG4BKI
-XN--CLCHC0EA0B2G2A9GCD
-XN--CZR694B
-XN--CZRS0T
-XN--CZRU2D
-XN--D1ACJ3B
-XN--D1ALF
-XN--E1A4C
-XN--ECKVDTC9D
-XN--EFVY88H
-XN--ESTV75G
-XN--FCT429K
-XN--FHBEI
-XN--FIQ228C5HS
-XN--FIQ64B
-XN--FIQS8S
-XN--FIQZ9S
-XN--FJQ720A
-XN--FLW351E
-XN--FPCRJ9C3D
-XN--FZC2C9E2C
-XN--FZYS8D69UVGM
-XN--G2XX48C
-XN--GCKR3F0F
-XN--GECRJ9C
-XN--GK3AT1E
-XN--H2BRJ9C
-XN--HXT814E
-XN--I1B6B1A6A2E
-XN--IMR513N
-XN--IO0A7I
-XN--J1AEF
-XN--J1AMH
-XN--J6W193G
-XN--JLQ61U9W7B
-XN--JVR189M
-XN--KCRX77D1X4A
-XN--KPRW13D
-XN--KPRY57D
-XN--KPU716F
-XN--KPUT3I
-XN--L1ACC
-XN--LGBBAT1AD8J
-XN--MGB9AWBF
-XN--MGBA3A3EJT
-XN--MGBA3A4F16A
-XN--MGBA7C0BBN0A
-XN--MGBAAM7A8H
-XN--MGBAB2BD
-XN--MGBAI9AZGQP6J
-XN--MGBAYH7GPA
-XN--MGBB9FBPOB
-XN--MGBBH1A71E
-XN--MGBC0A9AZCG
-XN--MGBCA7DZDO
-XN--MGBERP4A5D4AR
-XN--MGBI4ECEXP
-XN--MGBPL2FH
-XN--MGBT3DHD
-XN--MGBTX2B
-XN--MGBX4CD0AB
-XN--MIX891F
-XN--MK1BU44C
-XN--MXTQ1M
-XN--NGBC5AZD
-XN--NGBE9E0A
-XN--NODE
-XN--NQV7F
-XN--NQV7FS00EMA
-XN--NYQY26A
-XN--O3CW4H
-XN--OGBPF8FL
-XN--P1ACF
-XN--P1AI
-XN--PBT977C
-XN--PGBS0DH
-XN--PSSY2U
-XN--Q9JYB4C
-XN--QCKA1PMC
-XN--QXAM
-XN--RHQV96G
-XN--ROVU88B
-XN--S9BRJ9C
-XN--SES554G
-XN--T60B56A
-XN--TCKWE
-XN--TIQ49XQYJ
-XN--UNUP4Y
-XN--VERMGENSBERATER-CTB
-XN--VERMGENSBERATUNG-PWB
-XN--VHQUV
-XN--VUQ861B
-XN--W4R85EL8FHU5DNRA
-XN--W4RS40L
-XN--WGBH1C
-XN--WGBL6A
-XN--XHQ521B
-XN--XKC2AL3HYE2A
-XN--XKC2DL3A5EE0H
-XN--Y9A3AQ
-XN--YFRO4I67O
-XN--YGBI2AMMX
-XN--ZFR164B
-XPERIA
-XXX
-XYZ
-YACHTS
-YAHOO
-YAMAXUN
-YANDEX
-YE
-YODOBASHI
-YOGA
-YOKOHAMA
-YOU
-YOUTUBE
-YT
-YUN
-ZA
-ZAPPOS
-ZARA
-ZERO
-ZIP
-ZIPPO
-ZM
-ZONE
-ZUERICH
-ZW
diff --git a/tools/deps.toml b/tools/deps.toml
index 0500703..053b474 100644
--- a/tools/deps.toml
+++ b/tools/deps.toml
@@ -1,8 +1,8 @@
 [versions]
 antlr = "3.5.2"
 autoValueGson = "1.3.1"
-bouncyCastle = "1.85"
-byteBuddy = "1.18.12"
+bouncyCastle = "1.86"
+byteBuddy = "1.18.14"
 caffeine = "2.9.2"
 commonmark = "0.24.0"
 greenmail = "1.5.5"
@@ -37,7 +37,7 @@
 autotransient = { module = "io.sweers.autotransient:autotransient", version = "1.0.0" }
 bcpg-jdk18on = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncyCastle" }
 bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncyCastle" }
-bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.85.2" }
+bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.86" }
 bcutil-jdk18on = { module = "org.bouncycastle:bcutil-jdk18on", version.ref = "bouncyCastle" }
 byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byteBuddy" }
 byte-buddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
@@ -54,7 +54,7 @@
 commons-net = { module = "commons-net:commons-net", version = "3.6" }
 commons-pool = { module = "commons-pool:commons-pool", version = "1.5.5" }
 commons-text = { module = "org.apache.commons:commons-text", version = "1.15.0" }
-commons-validator = { module = "commons-validator:commons-validator", version = "1.6" }
+commons-validator = { module = "commons-validator:commons-validator", version = "1.11.0" }
 diffutils = { module = "io.github.java-diff-utils:java-diff-utils", version = "4.16" }
 failureaccess = { module = "com.google.guava:failureaccess", version = "1.0.3" }
 flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version = "0.64.0:lib" }
@@ -87,7 +87,7 @@
 jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.1" }
 junit = { module = "junit:junit", version = "4.13.2" }
 mime-util = { module = "eu.medsea.mimeutil:mime-util", version = "2.1.3" }
-mockito-core = { module = "org.mockito:mockito-core", version = "5.23.0" }
+mockito-core = { module = "org.mockito:mockito-core", version = "5.24.0" }
 objenesis = { module = "org.objenesis:objenesis", version = "3.4" }
 prolog-cafeteria = { module = "com.googlecode.prolog-cafe:prolog-cafeteria", version.ref = "prolog" }
 prolog-compiler = { module = "com.googlecode.prolog-cafe:prolog-compiler", version.ref = "prolog" }
diff --git a/tools/java_deps.MODULE.bazel b/tools/java_deps.MODULE.bazel
index e75c3ec..837dd14 100644
--- a/tools/java_deps.MODULE.bazel
+++ b/tools/java_deps.MODULE.bazel
@@ -2,7 +2,7 @@
 #
 # This file is included from the root MODULE.bazel via include().
 # It contains the rules_jvm_external configuration for Gerrit:
-#  - forced artifact versions to keep Gerrit and JGit aligned
+#  - pinned root artifact versions to keep Gerrit and JGit aligned
 #  - excluded artifacts to prevent duplicate classes and unwanted runtimes
 #  - TOML-based dependency import + lockfile enforcement
 #
@@ -103,40 +103,6 @@
     _OTHER_EXCLUDES
 )
 
-# Root-level dependency versions declared by Gerrit must take precedence over
-# versions contributed by layered modules (e.g. JGit) when resolving the shared
-# maven.install("external_deps") repository.
-#
-# rules_jvm_external currently lacks a mechanism for the root module to globally
-# enforce its declared versions across the layered Maven graph when using
-# bzlmod. In particular, version_conflict_policy = "pinned" still fails early
-# with duplicate artifacts when multiple modules contribute different versions.
-#
-# As a workaround, we explicitly force the root-selected versions here via
-# maven.amend_artifact(force_version = "true") for GA coordinates that may also
-# be introduced transitively by JGit.
-#
-# Versions themselves are sourced from deps.toml and the pinned lockfile.
-#
-# TODO(davido): Remove this workaround once global root version enforcement is
-# supported in rules_jvm_external:
-# https://github.com/bazel-contrib/rules_jvm_external/issues/1549
-_GERRIT_FORCED_ARTIFACTS = [
-    "args4j:args4j",
-    "com.googlecode.javaewah:JavaEWAH",
-    "commons-codec:commons-codec",
-    "org.apache.commons:commons-lang3",
-]
-
-[
-    maven.amend_artifact(
-        name = "external_deps",
-        coordinates = coord,
-        force_version = "true",
-    )
-    for coord in _GERRIT_FORCED_ARTIFACTS
-]
-
 # We consume Guice's unshaded artifact (published as the "classes"
 # classifier) plus an external OW2 ASM (see //lib/guice and tools/nongoogle.toml),
 # because the ASM shaded into the default guice jar cannot read Java 25 class
diff --git a/tools/nongoogle.toml b/tools/nongoogle.toml
index ea53a0d..d69d829 100644
--- a/tools/nongoogle.toml
+++ b/tools/nongoogle.toml
@@ -11,7 +11,7 @@
 guava = "33.5.0-jre"
 guice = "6.0.0"
 lucene = "10.5.1"
-slf4j = "2.0.18"
+slf4j = "2.0.20"
 sshd = "2.19.0"
 truth = "1.4.4"
 
diff --git a/tools/patches/BUILD b/tools/patches/BUILD
new file mode 100644
index 0000000..ebb6cc0
--- /dev/null
+++ b/tools/patches/BUILD
@@ -0,0 +1 @@
+exports_files(["protobuf-36.2-no-rust.patch"])
diff --git a/tools/patches/protobuf-36.2-no-rust.patch b/tools/patches/protobuf-36.2-no-rust.patch
new file mode 100644
index 0000000..9860606
--- /dev/null
+++ b/tools/patches/protobuf-36.2-no-rust.patch
@@ -0,0 +1,20 @@
+--- a/MODULE.bazel
++++ b/MODULE.bazel
+@@ -40,7 +40,7 @@
+ bazel_dep(name = "rules_license", version = "1.0.0")
+ bazel_dep(name = "rules_pkg", version = "1.0.1")
+ bazel_dep(name = "rules_python", version = "1.6.0")
+-bazel_dep(name = "rules_rust", version = "0.69.0")
++bazel_dep(name = "rules_rust", version = "0.69.0", dev_dependency = True)
+ 
+ bazel_dep(name = "rules_ruby", version = "0.20.1", dev_dependency = True)
+ 
+@@ -193,7 +193,7 @@
+     versions = ["1.85.0"],
+ )
+ 
+-crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
++crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate", dev_dependency = True)
+ crate.spec(
+     package = "googletest",
+     version = ">0.0.0",
diff --git a/webapp/diffy.png b/webapp/diffy.png
new file mode 100644
index 0000000..8400e95
--- /dev/null
+++ b/webapp/diffy.png
Binary files differ